CRTP в игровых механиках
В мире разработки игр часто возникает необходимость создавать гибкие и производительные системы. Одним из мощных инструментов в C++ для достижения этой цели является CRTP — Curiously Recurring Template Pattern. Сегодня мы разберём, как CRTP может помочь в создании игровых механик, и реализуем простой пример с использованием библиотеки SFML.
Что такое CRTP?
CRTP — это шаблонный паттерн в C++, где базовый класс является шаблоном, а производный класс передаётся в качестве параметра шаблона самому себе. Это позволяет базовому классу вызывать методы производного класса без использования виртуальных функций, что даёт выигрыш в производительности за счёт статической диспетчеризации.
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
// Реализация в производном классе
}
};Зачем использовать CRTP в играх?
- Производительность: Виртуальные функции добавляют накладные расходы из-за таблицы vtable. CRTP позволяет обойтись без них.
- Гибкость: Легко добавлять новые типы объектов с общей логикой.
- Типобезопасность: Компилятор проверяет корректность типов на этапе компиляции.
В игровых механиках это особенно полезно для систем вроде управления объектами (игроки, враги, снаряды), где нужно минимизировать задержки и упростить расширение.
Cистема игровых объектов с SFML
Давайте создадим простую систему игровых сущностей с использованием CRTP и SFML. Мы реализуем базовый класс GameObject, от которого будут наследоваться Player и Enemy. Каждый объект будет обновляться и отрисовываться через SFML.
Перед началом убедитесь, что у вас установлена библиотека SFML.
Создадим абстрактный базовый класс IGameObject и шаблонный класс с CRTP для реализации общей логики игровых объектов GameObject.
#pragma once
#include <SFML/Graphics.hpp>
// Абстрактный базовый класс для всех игровых объектов
class IGameObject {
public:
// Виртуальный деструктор для корректного удаления объектов
virtual ~IGameObject() = default;
// Чисто виртуальный метод для обновления состояния объекта
virtual void update(float deltaTime) = 0;
// Чисто виртуальный метод для отрисовки объекта
virtual void draw(sf::RenderWindow& window) = 0;
// Чисто виртуальный метод для получения позиции объекта
virtual sf::Vector2f getPosition() const = 0;
};#pragma once
#include <SFML/Graphics.hpp>
// Шаблонный класс с CRTP для реализации общей логики игровых объектов
template <typename Derived>
class GameObject : public IGameObject {
public:
// Переопределяем метод update, вызывая реализацию из производного класса
void update(float deltaTime) override {
static_cast<Derived*>(this)->updateImpl(deltaTime);
}
// Переопределяем метод draw, вызывая реализацию из производного класса
void draw(sf::RenderWindow& window) override {
static_cast<Derived*>(this)->drawImpl(window);
}
// Переопределяем метод getPosition, вызывая реализацию из производного класса
sf::Vector2f getPosition() const override {
return static_cast<const Derived*>(this)->getPositionImpl();
}
};- update обновляет состояние объекта.
- draw отвечает за отрисовку.
- getPosition возвращает позицию объекта.
Методы вызывают соответствующие реализации в производных классах через static_cast.
Теперь определим два игровых объекта: Player (игрок) и Enemy (враг).
#pragma once
#include "GameObject.h"
// Класс игрока, наследующийся от GameObject с использованием CRTP
class Player : public GameObject<Player> {
private:
// Графический объект — круг для представления игрока
sf::CircleShape shape;
// Позиция игрока на экране
sf::Vector2f position = sf::Vector2f(200.0f, 200.0f);
// Скорость перемещения игрока
float speed = 200.0f;
public:
// Конструктор игрока
Player() {
// Устанавливаем радиус круга
shape.setRadius(20.0f);
// Задаём зелёный цвет
shape.setFillColor(sf::Color::Green);
// Устанавливаем позицию круга
shape.setPosition(position);
}
// Реализация обновления состояния игрока
void updateImpl(float deltaTime) {
// Движение влево
if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) position.x -= speed * deltaTime;
// Движение вправо
if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) position.x += speed * deltaTime;
// Движение вверх
if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) position.y -= speed * deltaTime;
// Движение вниз
if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) position.y += speed * deltaTime;
// Обновляем позицию графического объекта
shape.setPosition(position);
}
// Реализация отрисовки игрока
void drawImpl(sf::RenderWindow& window) const {
// Отрисовываем круг в окне
window.draw(shape);
}
// Реализация получения позиции игрока
sf::Vector2f getPositionImpl() const {
// Возвращаем текущую позицию
return position;
}
};#pragma once
#include "GameObject.h"
// Класс врага, наследующийся от GameObject с использованием CRTP
class Enemy : public GameObject<Enemy> {
private:
// Графический объект — прямоугольник для врага
sf::RectangleShape shape;
// Позиция врага на экране
sf::Vector2f position = {400.0f, 300.0f};
// Скорость перемещения врага
float speed = 100.0f;
public:
// Конструктор врага
Enemy() {
// Устанавливаем размер прямоугольника
shape.setSize({ 30.0f, 30.0f });
// Задаём красный цвет
shape.setFillColor(sf::Color::Red);
// Устанавливаем позицию прямоугольника
shape.setPosition(position);
}
// Реализация обновления состояния врага
void updateImpl(float deltaTime) {
// Движение врага по горизонтали
position.x += speed * deltaTime;
// Разворот при достижении границ
if (position.x > 700 || position.x < 100) speed = -speed;
// Обновляем позицию графического объекта
shape.setPosition(position);
}
// Реализация отрисовки врага
void drawImpl(sf::RenderWindow& window) const {
// Отрисовываем прямоугольник в окне
window.draw(shape);
}
// Реализация получения позиции врага
sf::Vector2f getPositionImpl() const {
// Возвращаем текущую позицию
return position;
}
};Теперь соберём всё вместе в главном файле.
#include <SFML/Graphics.hpp>
#include <vector>
#include <memory>
#include "IGameObject.h"
#include "GameObject.h"
#include "Player.h"
#include "Enemy.h"
// Основная функция программы
int main() {
// Создаём окно 800x600
sf::RenderWindow window(sf::VideoMode(800, 600), "CRTP Game Example");
// Ограничиваем частоту кадров до 60 FPS
window.setFramerateLimit(60);
// Вектор умных указателей для хранения всех игровых объектов
std::vector<std::unique_ptr<IGameObject>> objects;
// Добавляем игрока в вектор
objects.push_back(std::make_unique<Player>());
// Добавляем врага в вектор
objects.push_back(std::make_unique<Enemy>());
// Часы для расчёта времени между кадрами
sf::Clock clock;
// Главный игровой цикл
while (window.isOpen()) {
sf::Event event;
// Обрабатываем события
while (window.pollEvent(event)) {
// Закрываем окно при нажатии на крестик
if (event.type == sf::Event::Closed) window.close();
}
// Вычисляем время с последнего кадра
float deltaTime = clock.restart().asSeconds();
// Обновляем состояние всех объектов
for (auto const& obj : objects) {
obj->update(deltaTime);
}
// Очищаем окно перед отрисовкой
window.clear();
// Отрисовываем все объекты
for (auto const& obj : objects) {
obj->draw(window);
}
// Отображаем нарисованное на экране
window.display();
}
return 0;
}Как это работает?
Player управляется клавишами W, A, S, D и отображается как зелёный круг.
Enemy движется автоматически влево-вправо и отображается как красный квадрат.
Базовый класс GameObject через CRTP вызывает методы конкретных классов (Player или Enemy) без виртуальных функций.
Преимущества подхода
Скорость: Отсутствие vtable ускоряет вызовы методов.
Расширяемость: Чтобы добавить новый тип объекта (например, Projectile), достаточно унаследовать его от GameObject<Projectile> и реализовать нужные методы.
Простота: Логика обновления и отрисовки сосредоточена в одном месте.
Итог
CRTP — это мощный инструмент для игровых движков, где важны производительность и гибкость. В примере с SFML мы создали систему объектов, которую легко расширить без потери скорости. Попробуйте добавить новые сущности вроде снарядов или бонусов — это займёт всего несколько строк кода!
vtable (от англ. virtual table) — это механизм в C++, который используется для реализации динамического полиморфизма через виртуальные функции. Когда вы объявляете функцию в базовом классе как virtual, компилятор создаёт специальную таблицу указателей (vtable) для каждого класса, который содержит виртуальные функции. Эта таблица хранит адреса реализаций виртуальных методов для конкретного класса.