Прежде чем мы начнем заниматься игровой механикой, нам необходимо подготовить каркас нашего приложения. В игре будут задействованы несколько сторонних библиотек, большинство из которых уже были описаны на предыдущих уроках. Если нам потребуется какая-нибудь новая библиотека, то я обязательно о ней детально расскажу.
Каркас игры
Вначале для нашей игры «Breakout» определим класс Game, который будет содержать весь соответствующий код рендеринга и геймплея. Идея создания подобного игрового класса заключается в том, что он организует наш игровой код, а также отделит код создания окна и вывода графики от кода с игровой логикой. Таким образом, мы без особых проблем сможем использовать тот же самый класс в совершенно другой оконной библиотеке (например, SDL или SFML).
Примечание: Существуют тысячи различных способов абстрагирования и обобщения игрового/графического кода на классы и объекты. То, с чем вы встретитесь на следующих уроках, — это всего лишь один (относительно простой) подход к решению данного вопроса. Но если вы чувствуете в себе силы и уверены, что можете сделать лучше, то ничего не мешает вам попробовать придумать свою собственную реализацию.
Класс Game содержит функцию инициализации, функцию обновления, функцию обработки игрового ввода с клавиатуры и функцию рендеринга:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Game { public: // Игровое состояние GameState State; bool Keys[1024]; unsigned int Width, Height; // Конструктор Game(unsigned int width, unsigned int height); // Деструктор ~Game(); // Инициализация начального состояния игры (загрузка всех шейдеров, текстур, уровней) void Init(); // Игровой цикл void ProcessInput(float dt); void Update(float dt); void Render(); }; |
Мы инициализируем игру параметрами разрешения окна (ширина и высота), в котором мы собираемся играть, и используем функцию Init() для загрузки шейдеров, текстур и инициализации начального состояния всего игрового процесса. Обрабатывать коды нажимаемых клавиш клавиатуры, сохраняемых в массиве Keys
, мы будем при помощи функции ProcessInput(), и обновлять все события игрового процесса (например, перемещения игрока/мяча) в функции Update(). Наконец, визуализация игры будет происходить с помощью функции Render(). Обратите внимание, что мы отделяем логику перемещения объектов от логики рендеринга.
Класс Game содержит переменную State
типа GameState
. Определение данного типа приведено ниже:
1 2 3 4 5 6 |
// Описываем текущее состояние игры enum GameState { GAME_ACTIVE, GAME_MENU, GAME_WIN }; |
Благодаря вышеописанной переменной мы можем отслеживать, в каком состоянии находится игра в данный момент. Таким образом, у нас появляется возможность изменить параметры рендеринга и/или обработки, в зависимости от её текущего состояния.
На данный момент функции игрового класса являются пустыми, нам еще предстоит написать непосредственный код игры, но ниже представлены уже готовые заголовочный файл и файл реализации игрового класса.
#Класс 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 |
#ifndef GAME_H #define GAME_H #include <glad/glad.h> #include <GLFW/glfw3.h> // Описываем текущее состояние игры enum GameState { GAME_ACTIVE, GAME_MENU, GAME_WIN }; // Класс Game содержит все связанные с игрой параметры состояния и различный функционал. // Он объединяет все игровые данные в один класс, обеспечивая тем самым простой доступ к каждому из компонентов класса и возможность управления ими class Game { public: // Игровое состояние GameState State; Bool Keys[1024]; unsigned int Width, Height; // Конструктор 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 |
#include "game.h" Game::Game(unsigned int width, unsigned int height) : State(GAME_ACTIVE), Keys(), Width(width), Height(height) { } Game::~Game() { } void Game::Init() { } void Game::Update(float dt) { } void Game::ProcessInput(float dt) { } void Game::Render() { } |
Служебные функции
Поскольку мы создаем большое приложение, то будем часто использовать текстуры и шейдеры. Таким образом, имеет смысл создать для них более простой в использовании интерфейс, как это было сделано на одном из предыдущих уроков, где мы создали шейдерный класс.
Поэтому давайте определим шейдерный класс, который генерирует скомпилированный шейдер (или выдаст сообщения об ошибках, если компиляция потерпит неудачу) на основе двух или трех (если присутствует геометрический шейдер) передаваемых ему строк. Шейдерный класс будет иметь множество служебных функций для быстрого задания значений uniform-переменных. Мы также определим текстурный класс, задача которого состоит в генерировании 2D-текстурного изображения (в зависимости от определенных свойств) на основе массива байтов данных и заданных параметров ширины/высоты изображения. Опять же, в текстурном классе будут представлены некоторые служебные функции.
Мы не будем углубляться в детали классов, так как вы уже и сами способны легко понять суть и принципы их работы. Ниже приведены заголовочный файл и файл реализации класса Shader.
#Класс Shader
Заголовочный файл — shader.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 |
#ifndef SHADER_H #define SHADER_H #include <string> #include <glad/glad.h> #include <glm/glm.hpp> #include <glm/gtc/type_ptr.hpp> // Шейдерный объект общего назначения. // Компилируется из файла, генерирует сообщения об ошибках времени компиляции/связывания // и содержит несколько служебных функций для удобства управления class Shader { public: // Состояние unsigned int ID; // Конструктор Shader() { } // Устанавливаем текущий шейдер в качестве активного Shader &Use(); // Компилируем шейдер из переданного исходного кода void Compile(const char *vertexSource, const char *fragmentSource, const char *geometrySource = nullptr); // примечание: Исходный код геометрического шейдера является опциональным // Служебные функции void SetFloat (const char *name, float value, bool useShader = false); void SetInteger (const char *name, int value, bool useShader = false); void SetVector2f (const char *name, float x, float y, bool useShader = false); void SetVector2f (const char *name, const glm::vec2 &value, bool useShader = false); void SetVector3f (const char *name, float x, float y, float z, bool useShader = false); void SetVector3f (const char *name, const glm::vec3 &value, bool useShader = false); void SetVector4f (const char *name, float x, float y, float z, float w, bool useShader = false); void SetVector4f (const char *name, const glm::vec4 &value, bool useShader = false); void SetMatrix4 (const char *name, const glm::mat4 &matrix, bool useShader = false); private: // Проверяем, не произошла ли ошибка компиляции/связывания, и если да, то выводим логи ошибок void checkCompileErrors(unsigned int object, std::string type); }; #endif |
Файл реализации — shader.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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
#include "shader.h" #include <iostream> Shader &Shader::Use() { glUseProgram(this->ID); return *this; } void Shader::Compile(const char* vertexSource, const char* fragmentSource, const char* geometrySource) { unsigned int sVertex, sFragment, gShader; // Вершинный шейдер sVertex = glCreateShader(GL_VERTEX_SHADER); glShaderSource(sVertex, 1, &vertexSource, NULL); glCompileShader(sVertex); checkCompileErrors(sVertex, "VERTEX"); // Фрагментный шейдер sFragment = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(sFragment, 1, &fragmentSource, NULL); glCompileShader(sFragment); checkCompileErrors(sFragment, "FRAGMENT"); // Если задан исходный код геометрического шейдера, то компилируем и его if (geometrySource != nullptr) { gShader = glCreateShader(GL_GEOMETRY_SHADER); glShaderSource(gShader, 1, &geometrySource, NULL); glCompileShader(gShader); checkCompileErrors(gShader, "GEOMETRY"); } // Шейдерная программа this->ID = glCreateProgram(); glAttachShader(this->ID, sVertex); glAttachShader(this->ID, sFragment); if (geometrySource != nullptr) glAttachShader(this->ID, gShader); glLinkProgram(this->ID); checkCompileErrors(this->ID, "PROGRAM"); // Удаляем шейдеры, так как теперь они связаны с нашей программой и больше не нужны glDeleteShader(sVertex); glDeleteShader(sFragment); if (geometrySource != nullptr) glDeleteShader(gShader); } void Shader::SetFloat(const char *name, float value, bool useShader) { if (useShader) this->Use(); glUniform1f(glGetUniformLocation(this->ID, name), value); } void Shader::SetInteger(const char *name, int value, bool useShader) { if (useShader) this->Use(); glUniform1i(glGetUniformLocation(this->ID, name), value); } void Shader::SetVector2f(const char *name, float x, float y, bool useShader) { if (useShader) this->Use(); glUniform2f(glGetUniformLocation(this->ID, name), x, y); } void Shader::SetVector2f(const char *name, const glm::vec2 &value, bool useShader) { if (useShader) this->Use(); glUniform2f(glGetUniformLocation(this->ID, name), value.x, value.y); } void Shader::SetVector3f(const char *name, float x, float y, float z, bool useShader) { if (useShader) this->Use(); glUniform3f(glGetUniformLocation(this->ID, name), x, y, z); } void Shader::SetVector3f(const char *name, const glm::vec3 &value, bool useShader) { if (useShader) this->Use(); glUniform3f(glGetUniformLocation(this->ID, name), value.x, value.y, value.z); } void Shader::SetVector4f(const char *name, float x, float y, float z, float w, bool useShader) { if (useShader) this->Use(); glUniform4f(glGetUniformLocation(this->ID, name), x, y, z, w); } void Shader::SetVector4f(const char *name, const glm::vec4 &value, bool useShader) { if (useShader) this->Use(); glUniform4f(glGetUniformLocation(this->ID, name), value.x, value.y, value.z, value.w); } void Shader::SetMatrix4(const char *name, const glm::mat4 &matrix, bool useShader) { if (useShader) this->Use(); glUniformMatrix4fv(glGetUniformLocation(this->ID, name), 1, false, glm::value_ptr(matrix)); } void Shader::checkCompileErrors(unsigned int object, std::string type) { int success; char infoLog[1024]; if (type != "PROGRAM") { glGetShaderiv(object, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(object, 1024, NULL, infoLog); std::cout << "| ERROR::SHADER: Compile-time error: Type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } else { glGetProgramiv(object, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(object, 1024, NULL, infoLog); std::cout << "| ERROR::Shader: Link-time error: Type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl; } } } |
#Класс Texture2D
Заголовочный файл — texture.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 TEXTURE_H #define TEXTURE_H #include <glad/glad.h> // Класс Texture2D хранит в себе текстуру и может производить её конфигурирование. // Он также содержит служебные функции для простоты управления class Texture2D { public: // Содержит ID объекта текстуры, используемый для всех операций с конкретной текстурой unsigned int ID; // Размеры текстурного изображения unsigned int Width, Height; // ширина и высота (в пикселях) загруженного изображения // Формат текстуры unsigned int Internal_Format; // внутренний формат текстуры unsigned int Image_Format; // формат загружаемого изображения // Конфигурация текстуры unsigned int Wrap_S; // режим наложения по оси S unsigned int Wrap_T; // режим наложения по оси T unsigned int Filter_Min; // режим фильтрации, если пикселей текстуры < пикселей экрана unsigned int Filter_Max; // режим фильтрации, если пикселей текстуры > пикселей экрана // Конструктор (устанавливает заданные по умолчанию значения) Texture2D(); // Генерируем текстуру из изображения void Generate(unsigned int width, unsigned int height, unsigned char* data); // Связываем текстуру в виде текущего активного GL_TEXTURE_2D текстурного объекта void Bind() const; }; #endif |
Файл реализации — texture.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 |
#include <iostream> #include "texture.h" Texture2D::Texture2D() : Width(0), Height(0), Internal_Format(GL_RGB), Image_Format(GL_RGB), Wrap_S(GL_REPEAT), Wrap_T(GL_REPEAT), Filter_Min(GL_LINEAR), Filter_Max(GL_LINEAR) { glGenTextures(1, &this->ID); } void Texture2D::Generate(unsigned int width, unsigned int height, unsigned char* data) { this->Width = width; this->Height = height; // Создаем текстуру glBindTexture(GL_TEXTURE_2D, this->ID); glTexImage2D(GL_TEXTURE_2D, 0, this->Internal_Format, width, height, 0, this->Image_Format, GL_UNSIGNED_BYTE, data); // Задаем для текстуры режимы наложения и фильтрации glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, this->Wrap_S); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, this->Wrap_T); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, this->Filter_Min); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, this->Filter_Max); // Отвязываем текстуру glBindTexture(GL_TEXTURE_2D, 0); } void Texture2D::Bind() const { glBindTexture(GL_TEXTURE_2D, this->ID); } |
Обратите внимание, что текущий класс текстур предназначен исключительно для 2D-текстур, но его можно легко расширить для реализации поддержки и других типов текстур.
Управление ресурсами
Хотя шейдерный и текстурный классы прекрасно функционируют сами по себе, для их инициализации требуется либо массив байтов, либо список строк. Мы могли бы легко встроить код загрузки файлов в сами классы, но это немного нарушает принцип единственной ответственности. Предпочтительнее, чтобы эти классы фокусировались только непосредственно на своих объектах, т.е. на текстурах или шейдерах (соответственно), а не на механизме загрузки их данных из файлов.
По этой причине часто рассматривается более организованный подход к созданию единого объекта (или сущности), предназначенного для загрузки всех связанных с игрой ресурсов, называемый менеджером ресурсов. Существует несколько подходов к созданию менеджера ресурсов; на этом уроке мы будем использовать одноэлементный статический менеджер ресурсов, который (в силу своей статической природы) всегда доступен на протяжении всего проекта. Он содержит в себе все загруженные ресурсы и соответствующие функции для их загрузки.
Использование одноэлементного класса со статической функциональностью имеет ряд преимуществ и недостатков, причем к основным его недостаткам относятся потеря некоторых свойств ООП и меньший контроль над построением/разрушением объектов. Однако в относительно небольших проектах, подобных нашему, с ним очень легко работать.
#Класс ResourceManager
Заголовочный файл — resource_manager.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 RESOURCE_MANAGER_H #define RESOURCE_MANAGER_H #include <map> #include <string> #include <glad/glad.h> #include "texture.h" #include "shader.h" // Статический одноэлементный класс ResourceManager, содержащий несколько функций для загрузки текстур и шейдеров. // Каждая загруженная текстура и/или шейдер сохраняются для дальнейшего использования. // Все функции и ресурсы статичны class ResourceManager { public: // Хранилище ресурсов static std::map<std::string, Shader> Shaders; static std::map<std::string, Texture2D> Textures; // Загружаем (и генерируем) шейдерную программу из файла загрузки исходного кода вершинных, фрагментных (и геометрических) шейдеров. // Если gShaderFile != nullptr, то также загружается и геометрический шейдер static Shader LoadShader(const char *vShaderFile, const char *fShaderFile, const char *gShaderFile, std::string name); // Получаем сохраненный шейдер static Shader GetShader(std::string name); // Загружаем (и генерируем) текстуру из файла static Texture2D LoadTexture(const char *file, bool alpha, std::string name); // Получаем сохраненную текстуру static Texture2D GetTexture(std::string name); // Корректное освобождение памяти всех загруженных ресурсов static void Clear(); private: // private-конструктор, т.к. мы не хотим, чтобы создавались реальные объекты менеджера ресурсов. Его члены и функции должны быть общедоступными (статичными) ResourceManager() { } // Загружаем и генерируем шейдер из файла static Shader loadShaderFromFile(const char *vShaderFile, const char *fShaderFile, const char *gShaderFile = nullptr); // Загружаем текстуру из файла static Texture2D loadTextureFromFile(const char *file, bool alpha); }; #endif |
Файл реализации — resource_manager.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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
#include "resource_manager.h" #include <iostream> #include <sstream> #include <fstream> #include "stb_image.h" // Создание экземпляров статических переменных std::map<std::string, Texture2D> ResourceManager::Textures; std::map<std::string, Shader> ResourceManager::Shaders; Shader ResourceManager::LoadShader(const char *vShaderFile, const char *fShaderFile, const char *gShaderFile, std::string name) { Shaders[name] = loadShaderFromFile(vShaderFile, fShaderFile, gShaderFile); return Shaders[name]; } Shader ResourceManager::GetShader(std::string name) { return Shaders[name]; } Texture2D ResourceManager::LoadTexture(const char *file, bool alpha, std::string name) { Textures[name] = loadTextureFromFile(file, alpha); return Textures[name]; } Texture2D ResourceManager::GetTexture(std::string name) { return Textures[name]; } void ResourceManager::Clear() { // (корректное) удаление всех шейдеров for (auto iter : Shaders) glDeleteProgram(iter.second.ID); // (корректное) удаление всех текстур for (auto iter : Textures) glDeleteTextures(1, &iter.second.ID); } Shader ResourceManager::loadShaderFromFile(const char *vShaderFile, const char *fShaderFile, const char *gShaderFile) { // 1. Получение исходного кода вершинного и фрагментного шейдеров std::string vertexCode; std::string fragmentCode; std::string geometryCode; try { // Открываем файлы std::ifstream vertexShaderFile(vShaderFile); std::ifstream fragmentShaderFile(fShaderFile); std::stringstream vShaderStream, fShaderStream; // Считываем содержимое файлового буфера в соответствующие потоки vShaderStream << vertexShaderFile.rdbuf(); fShaderStream << fragmentShaderFile.rdbuf(); // Закрываем файлы vertexShaderFile.close(); fragmentShaderFile.close(); // Конвертируем поток в строку vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); // Если присутствует путь геометрического шейдера, то загружаем геометрический шейдер if (gShaderFile != nullptr) { std::ifstream geometryShaderFile(gShaderFile); std::stringstream gShaderStream; gShaderStream << geometryShaderFile.rdbuf(); geometryShaderFile.close(); geometryCode = gShaderStream.str(); } } catch (std::exception e) { std::cout << "ERROR::SHADER: Failed to read shader files" << std::endl; } const char *vShaderCode = vertexCode.c_str(); const char *fShaderCode = fragmentCode.c_str(); const char *gShaderCode = geometryCode.c_str(); // 2. Теперь создаем объект шейдера из исходного кода Shader shader; shader.Compile(vShaderCode, fShaderCode, gShaderFile != nullptr ? gShaderCode : nullptr); return shader; } Texture2D ResourceManager::loadTextureFromFile(const char *file, bool alpha) { // Создаем объект текстуры Texture2D texture; if (alpha) { texture.Internal_Format = GL_RGBA; texture.Image_Format = GL_RGBA; } // Загружаем изображение int width, height, nrChannels; unsigned char* data = stbi_load(file, &width, &height, &nrChannels, 0); // Теперь генерируем текстуру texture.Generate(width, height, data); stbi_image_free(data); return texture; } |
Используя менеджер ресурсов, мы можем легко загружать в программу шейдеры:
1 2 3 4 5 |
Shader shader = ResourceManager::LoadShader("vertex.vs", "fragment.vs", nullptr, "test"); // После чего используем команду… shader.Use(); // …или… ResourceManager::GetShader("test").Use(); |
Описанный класс Game вместе с менеджером ресурсов и с легко управляемыми классами Shader и Texture2D образуют основу для следующих уроков, поскольку мы будем широко использовать данные классы для реализации игры «Breakout».
Программа
Теперь для игры необходимо создать окно и задействовать смешивание. Мы не включаем тестирование глубины, так как игра выполнена полностью в 2D. Все вершины определены с одинаковыми z-значениями, поэтому включение режима тестирования глубины было бы бесполезным и, скорее всего, вызвало бы z-конфликт.
Стартовый код игры «Breakout» относительно прост: мы создаем окно с GLFW, регистрируем несколько callback-функций, создаем игровой объект и распространяем всю соответствующую функциональность на игровой класс.
Главный файл программы — program.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 97 98 99 100 101 102 103 104 |
#include <glad/glad.h> #include <GLFW/glfw3.h> #include "game.h" #include "resource_manager.h" #include <iostream> // Объявления GLFW-функций void framebuffer_size_callback(GLFWwindow* window, int width, int height); void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode); // Ширина экрана const unsigned int SCREEN_WIDTH = 800; // Высота экрана const unsigned int SCREEN_HEIGHT = 600; Game Breakout(SCREEN_WIDTH, SCREEN_HEIGHT); int main(int argc, char *argv[]) { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif glfwWindowHint(GLFW_RESIZABLE, false); GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Breakout", nullptr, nullptr); glfwMakeContextCurrent(window); // GLAD: загрузка всех указателей на функции OpenGL if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } glfwSetKeyCallback(window, key_callback); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // Конфигурация OpenGL glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Инициализация игры Breakout.Init(); float deltaTime = 0.0f; float lastFrame = 0.0f; while (!glfwWindowShouldClose(window)) { // Вычисление дельты времени float currentFrame = glfwGetTime(); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; glfwPollEvents(); // Обрабатываем пользовательский ввод с клавиатуры Breakout.ProcessInput(deltaTime); // Обновляем состояние игры Breakout.Update(deltaTime); // Рендер glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); Breakout.Render(); glfwSwapBuffers(window); } // Освобождение всех ресурсов, загруженных с использованием менеджера ресурсов ResourceManager::Clear(); glfwTerminate(); return 0; } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) { // Когда пользователь нажимает клавишу Escape, мы устанавливаем для свойства WindowShouldClose значение true, закрывая приложение if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(window, true); if (key >= 0 && key < 1024) { if (action == GLFW_PRESS) Breakout.Keys[key] = true; else if (action == GLFW_RELEASE) Breakout.Keys[key] = false; } } void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // Убеждаемся, что окно просмотра соответствует новым размерам окна. // Обратите внимание, высота окна на Retina-дисплеях будет значительно больше, чем указано в программе glViewport(0, 0, width, height); } |
Запуск нашей программы должен привести к следующему результату:
GitHub / Часть №2: Подготовка к созданию игры «Breakout» на С++/OpenGL — Исходный код
Теперь у нас есть прочная основа для следующих уроков; мы будем постоянно расширять игровой класс, добавляя в него необходимый функционал.