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) для каждого класса, который содержит виртуальные функции. Эта таблица хранит адреса реализаций виртуальных методов для конкретного класса.