Пишем собственный игровой движок с помощью C++
С нуля создадим собственный игровой движок с помощью библиотеки SFML и C++, чтобы разобраться, как происходит создание ядра.
В этом проекте мы создадим собственный игровой движок на C++. Движок будет очень простым, но готовым к расширению возможностей. Конечная игра с использованием этого кода тоже крайне проста: наш персонаж сможет перемещаться влево и право, а из графики – только бэкграунд и фигурка персонажа.
Подготовка Visual Studio
Создадим новый проект в Visual Studio. Обратите внимание, что проект требует библиотеку SFML, поэтому если вы не настраивали окружение для нее, прочтите сначала небольшое руководство по настройке.
Настройка VS:
- Откройте Visual Studio и выберите File | New Project. В левом меню отметьте язык C++ и шаблон HelloSFML. Назовите проект Simple Game Engine.
- Выберите правой кнопкой мыши файл HelloSFML.cpp в окне Solution Explorer (под Source Files), далее – Rename и назовите файл Main. Это подходящее имя, так как файл будет содержать основную функцию.
- Откройте Main.cpp, удалите все содержимое файла.
- Переместите файл библиотеки SFML.dll из каталога Диск:\SFML\bin в Диск:\Visual Studio Stuff\Projects\Simple Game Engine\Simple Game Engine. Если названия в путях отличаются, вероятно, вы сделали отличную от руководства настройку.
Теперь можно приступить к коду. Исходный код и дополнительные ресурсы будут доступны на этой странице.
Проектируем собственный игровой движок
Самое важное – запуск движка, который будет происходить в файле Main.cpp, но им мы займемся немного позже.
Класс персонажа
Bob – простой класс для представления фигурки персонажа, управляемой игроком. Код класса будет легко расширяться, а что самое главное – его несложно переписать под любой другой игровой объект, который вы захотите добавить. Для этого потребуется заменить текстуру и описать поведение нового объекта в методе update().
Bob.h
Займемся заголовками класса. Выберите правой кнопкой Header Files в Solution Explorer и нажмите Add | New Item. В окне Add New Item выберите Header File (.h), затем в поле Name введите Bob. Нажмите Add и добавьте код заголовка класса:
#pragma once #include <SFML/Graphics.hpp> using namespace sf; class Bob { // Все private переменные могут быть доступны только внутри объекта private: // Позиция Боба Vector2f m_Position; // Объявляем объект Sprite Sprite m_Sprite; // Добавдяем текстуру Texture m_Texture; // Логические переменные для отслеживания направления движения bool m_LeftPressed; bool m_RightPressed; // Скорость Боба в пикселях в секунду float m_Speed; // Открытые методы public: // Настраиваем Боба в конструкторе Bob(); // Для отправки спрайта в главную функцию Sprite getSprite(); // Для движения Боба void moveLeft(); void moveRight(); // Прекращение движения void stopLeft(); void stopRight(); // Эта функция будет вызываться на каждый кадр void update(float elapsedTime); };
Здесь мы объявили объекты типа Texture и Sprite. Дальше мы свяжем эти объекты и любое действие на экране с объектом Sprite будет сопровождаться изображением Боба:
Кликните правой кнопкой, чтобы сохранить
Bob.cpp
Теперь приступим к описанию методов.
Выберите правой кнопкой мыши Source Files в Solution Explorer и откройте Add | New Item. В окне Add New Item кликните по C++ File (.cpp), а в поле Name укажите Bob.cpp. Теперь добавьте в файл код:
#include "stdafx.h" #include "bob.h" Bob::Bob() { // Вписываем в переменную скорость Боба m_Speed = 400; // Связываем текстуру и спрайт m_Texture.loadFromFile("bob.png"); m_Sprite.setTexture(m_Texture); // Устанавливаем начальную позицию Боба в пикселях m_Position.x = 500; m_Position.y = 800; } // Делаем приватный спрайт доступным для функции draw() Sprite Bob::getSprite() { return m_Sprite; } void Bob::moveLeft() { m_LeftPressed = true; } void Bob::moveRight() { m_RightPressed = true; } void Bob::stopLeft() { m_LeftPressed = false; } void Bob::stopRight() { m_RightPressed = false; } // Двигаем Боба на основании пользовательского ввода в этом кадре, // прошедшего времени и скорости void Bob::update(float elapsedTime) { if (m_RightPressed) { m_Position.x += m_Speed * elapsedTime; } if (m_LeftPressed) { m_Position.x -= m_Speed * elapsedTime; } // Теперь сдвигаем спрайт на новую позицию m_Sprite.setPosition(m_Position); }
В конструкторе мы установили значение переменной m_Speed на 400. Это значит, что Боб пересечет экран шириной в 1920 пикселей за 5 секунд. Также мы загрузили файл Bob.png в Texture и связали его с объектом Sprite. В переменных m_Position.x и m_Position.y установлено начальное положение Боба.
Функция update обрабатывает два If. Первое If проверяет, нажата ли правая кнопка (m_RightPressed), а второе следит за левой (m_LeftPressed). В каждом If скорость (m_Speed) умножается на elapsedTime. Переменная elapsedTime рассчитывается в функции Start движка (класс Engine). Им мы и займемся далее.
Пишем класс Engine
Класс Engine будет контролировать все остальное.
Engine.h
Добавим заголовок. Откройте окно Add New Item (так же, как для класса Bob), выберите Header File (.h) и в поле Name введите Engine.h. Добавьте в файл следующий код:
#pragma once #include <SFML/Graphics.hpp> #include "Bob.h"; using namespace sf; class Engine { private: RenderWindow m_Window; // Объявляем спрайт и текстуру для фона Sprite m_BackgroundSprite; Texture m_BackgroundTexture; // Экземпляр Боба Bob m_Bob; void input(); void update(float dtAsSeconds); void draw(); public: // Конструктор движка Engine(); // Функция старт вызовет все приватные функции void start(); };
Класс библиотеки SFML, RenderWIndow, используется для рендера всего, что есть на экране. Переменные Sprite и Texture нужны для создания фона. Также в заголовке мы создали экземпляр класса Bob.
Engine.cpp
В Engine.cpp мы опишем конструктор и функцию start. Создайте файл класса так же, как для Bob.cpp, и добавьте в него код:
#include "stdafx.h" #include "Engine.h" Engine::Engine() { // Получаем разрешение экрана, создаем окно SFML и View Vector2f resolution; resolution.x = VideoMode::getDesktopMode().width; resolution.y = VideoMode::getDesktopMode().height; m_Window.create(VideoMode(resolution.x, resolution.y), "Simple Game Engine", Style::Fullscreen); // Загружаем фон в текстуру // Подготовьте изображение под ваш размер экрана в редакторе m_BackgroundTexture.loadFromFile("background.jpg"); // Связываем спрайт и текстуру m_BackgroundSprite.setTexture(m_BackgroundTexture); } void Engine::start() { // Расчет времени Clock clock; while (m_Window.isOpen()) { // Перезапускаем таймер и записываем отмеренное время в dt Time dt = clock.restart(); float dtAsSeconds = dt.asSeconds(); input(); update(dtAsSeconds); draw(); } }
Функция конструктора получает разрешение экрана и разворачивает игру на весь экран с помощью m_Window.create. В конструкторе же загружается Texture и связывается с объектом Sprite.
Пример фонового изображения
Скачайте пример изображения или используйте любое другое на свое усмотрение. Переименуйте файл в background.jpg и поместите в каталог Simple Game Engine/Simple Game Engine.
Игровой цикл
Следующие три функции будут описаны каждая в своем файле, но при этом они должны быть частью Engine.h. Поэтому в начале каждого файла укажем директиву #include «Engine.h», так что Visual Studio будет знать, что мы делаем.
Обрабатываем пользовательский ввод
Создайте файл Input.cpp и добавьте в него код:
#include "stdafx.h" #include "Engine.h" void Engine::input() { // Обрабатываем нажатие Escape if (Keyboard::isKeyPressed(Keyboard::Escape)) { m_Window.close(); } // Обрабатываем нажатие клавиш движения if (Keyboard::isKeyPressed(Keyboard::A)) { m_Bob.moveLeft(); } else { m_Bob.stopLeft(); } if (Keyboard::isKeyPressed(Keyboard::D)) { m_Bob.moveRight(); } else { m_Bob.stopRight(); } }
Функция input обрабатывает нажатия клавиш через константу Keyboard::isKeyPressed, предоставляемую SFML. При нажатии Escape m_Window будет закрыто. Для клавиш A и D вызывается соответствующая функция движения.
Обновляем игровые объекты
Теперь опишем простую функцию update. Каждый игровой объект будет иметь собственную функцию update.
Создайте файл Update.cpp и добавьте в него код:
#include "stdafx.h" #include "Engine.h" using namespace sf; void Engine::update(float dtAsSeconds) { m_Bob.update(dtAsSeconds); }
Поскольку у нас пока только один объект "Боб", мы вызываем только функцию m_Bob.update.
Отрисовка сцены
Это последняя функция класса Engine. Создайте файл Draw.cpp и добавьте в него код:
#include "stdafx.h" #include "Engine.h" void Engine::draw() { // Стираем предыдущий кадр m_Window.clear(Color::White); // Отрисовываем фон m_Window.draw(m_BackgroundSprite); m_Window.draw(m_Bob.getSprite()); // Отображаем все, что нарисовали m_Window.display(); }
Экран очищается методом clear, затем отрисовывается фон. Первым делом должен быть отрисован фон, чтобы потом поверх него можно было отрисовать Боба.
Запускаем движок в main()
Теперь вернемся к нашему Main.cpp. Время добавить в него немного кода:
#include "stdafx.h" #include "Engine.h" int main() { // Объявляем экземпляр класса Engine Engine engine; // Вызываем функцию start engine.start(); // Останавливаем программу программу, когда движок остановлен return 0; }
Несколько слов в конце
Наш собственный игровой движок получился очень простым: он умеет только двигать главный объект и закрывать программу. Он не умеет обрабатывать столкновения, работать с интерфейсом и еще много чего. Однако он отлично описывает то, как строится ядро игрового проекта с нуля. К тому же, как мы уже выяснили, класс Bob расширяется и адаптируется под другие объекты, так что дайте волю фантазии и попробуйте поэкспериментировать с окружением.