Silver 19 ноября 2019
1
2782

Первый игровой движок на С++ и SFML

Пошаговый гайд по написанию простого 2D игрового движка на С++: твой уверенный старт в геймдеве!

Настройка окружения.

Прежде чем приступить к весёлой части с кодом, придётся пройти муторную, но важную часть с настройкой sfml. Данный гайд написан для Visual Studio 2019 на Windows. Установка для VS 2017 практически идентична, но могут быть незначительные отличия. Для других платформ крайне рекомендуется поискать гайд по настройке именно для вашего окружения. Ошибки на этом этапе, в лучшем случае, не дадут проекту скомпилироваться, в худшем – скомпилируются неправильно и вынесут вам мозг.

Переходим на главный сайт библиотеки и скачиваем последнюю стабильную версию (2.5.1 к моменту написания статьи). Выбираем для С++ 15(2017) 32-бита. Установка 64-битной версии идентична, для экономии времени сосредоточимся на более универсальной.

Распаковываем архив в удобную директорию, можно сразу сохранить в буфер путь до содержимого папки, в моем случае – это C:\SFML-2.5.1\.

Открываем VS-2019, создаем новое консольное приложение Windows, Называем проект FirstGameEngine, получаем консольный хеллоуворлд.

Теперь включаем концентрацию на максимум, задача довольно простая, но почти каждый допускает ошибку во время первой настройки. Открываем вкладку Проект, в самом низу выбираем свойства: FirstGameEngine.

Выставляем конфигурацию Все конфигурации и платформу Win32. Выбираем пункт С/С++ → Общие, находим Дополнительные каталоги включаемых файлов – здесь нужно указать путь до папки include. Вставляем путь из буфера и дописываем, как сделал я. Если сомневаетесь, нажмите изменить и найдите папку через эксплорер.

Спускаемся в Компоновщик → Дополнительные каталоги библиотек и проделываем тоже самое, но уже с папкой lib.

Переключаем Конфигурацию на Debug. Открываем Компоновщик → Ввод → Дополнительные зависимости. Копируем этот перечень sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-audio-d.lib; и аккуратно, не стирая и не перезаписывая уже имеющиеся записи, вставляем его в начало списка.

Переключаемся на Release и так же аккуратно вставляем sfml-graphics.lib;sfml-window.lib;sfml-system.lib;sfml-audio.lib;

Теперь поочередно запускаем Debug и Release, ловим по ошибке. Этот шаг необходим, чтобы студия создала папки проекта с exe-файлами. Сразу найдите эти каталоги. Их расположение зависит от настроек студии. Обычно они создаются на уровень выше папки с кодом. Теперь возвращаемся к распакованному архиву sfml, находим папку bin и копируем все бинарники с символом d в названии плюс openal32.bin в папку Debug, и то же самое без d + openal32.bin в Release.

Должна получиться такая картина:

Самая нудная часть позади.

Написание первого игрового движка на С++.

К файлу FirstGameEngine.cpp мы вернемся чуть позже. Его единственная задача – это запустить в функции main() наш движок.

Класс персонажа.

Bob – простой класс для представления фигурки персонажа, управляемой игроком. Код класса будет легко расширяться, а что самое главное – его несложно переписать под любой другой игровой объект, который вы захотите добавить. Для этого потребуется заменить текстуру и описать поведение нового объекта в методе update().

Bob.h

Создаем новый заголовочный файл. Кликаем Проект Добавить новый элемент Файл заголовка. Называем Bob.h и объявляем следующие поля и методы:

            #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 будет сопровождаться изображением Боба:

Сохраните это изображение в директорию FirstGameEngine/FirstGameEngine.

Bob.cpp

Приступим к реализации.

По той же схеме создаем файл Bob.cpp, только выбираем Файл С++. Теперь добавляем в файл код:

            #include "bob.h"
 
Bob::Bob()
{
    // Вписываем в переменную скорость Боба
    m_Speed = 400;
 
    // Связываем текстуру и спрайт
    m_Texture.loadFromFile("bob.png");
    m_Sprite.setTexture(m_Texture);     
 
    // Устанавливаем начальную позицию Боба в пикселях
    m_Position.x = 300;
    m_Position.y = 300;
 
}
 
// Делаем приватный спрайт доступным для функции 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.h

Создаем новый заголовочный файл 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

Здесь мы реализуем конструктор и функцию start(). Создаем файл класса так же, как для Bob.cpp, и добавляем в него код:

            #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 и поместите в каталог FirstGameEngine/FirstGameEngine.

Можете воспользоваться моим примером:

Игровой цикл.

Следующие три функции будут описаны каждая в отдельном файле, но при этом они должны быть частью Engine.h. Поэтому в начале каждого файла укажем директиву #include «Engine.h», так что Visual Studio будет знать, что мы делаем. Это делается для удобства дальнейшего масштабирования проекта.

Обрабатываем пользовательский ввод

            #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. Её задача вызвать аналогичную функцию обновления состояния у всех игровых объектов. В нашем случае это только Bob.

Создаём файл Update.cpp и добавьте в него код:

            #include "Engine.h"
 
using namespace sf;
 
void Engine::update(float dtAsSeconds)
{
    m_Bob.update(dtAsSeconds);
} 
        

Отрисовка экрана.

Это последняя функция класса Engine, её задача – отрисовать все объекты текущего экрана. Создайте файл Draw.cpp и добавьте в него код:

            #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. Первым делом должен быть отрисован фон, чтобы потом поверх него можно было поместить Боба.

Запускаем движок!

Теперь вернемся к FirstGameEngine.cpp.

            #include "Engine.h"
 
int main()
{
    // Объявляем экземпляр класса Engine
    Engine engine;
 
    // Вызываем функцию start
    engine.start();
 
    // Останавливаем программу программу, когда движок остановлен
    return 0;
} 
        

Наш игровой движок, как и положено первой работе, получился очень простым: он умеет только двигать главный объект и закрывать программу. Он не умеет обрабатывать столкновения, работать с интерфейсом и еще много чего. Возможно, этим мы займемся позже. Однако он отлично описывает то, как строится ядро игрового проекта с нуля. К тому же, в него довольно легко добавлять новые объекты и расширять функционал. Попробуйте, в качестве практики, добавить второго боба, управляемого клавишами J и L, у вас уже есть для этого все необходимые знания. Или поменяйте фон, если Боб выходит за границу экрана. Задачка уже посложней, но тоже вам по силам. =)

РУБРИКИ В СТАТЬЕ

Комментарии 1

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG