Создание игры на C++: что знать, план и первая игра с кодом

Создание игры на C++: что знать, план и первая игра с кодом Полезное

Создание игры на C++ сводится к трем вещам: логика игры на самом языке, игровой цикл (game loop), который раз за разом читает ввод, обновляет состояние и рисует кадр, и библиотека или движок, которые дают окно, клавиатуру, графику и звук. Стандартная библиотека C++ окон и графики не умеет, поэтому первую игру удобно написать в консоли, а затем перенести ту же логику в окно на SFML, raylib или SDL.

Ниже — словарь, выбор инструмента, рабочая консольная игра целиком, разбор цикла с фиксированным шагом, план до игры в окне и частые ошибки сборки. Консольные примеры собраны и запущены 24.09.2026 компиляторами Apple clang 21 и GCC 14 с флагом -std=c++17, вывод совпал; код использует только стандарт C++17, в MSVC для кириллицы в строках нужен флаг /utf-8.

Что нужно знать до начала: мини-словарь

  • Игровой цикл — повторяющаяся последовательность «ввод -> обновление -> отрисовка». Пока цикл крутится, игра живет.
  • Кадр и тик — кадр это одна отрисовка, тик это один шаг логики. В простых играх они совпадают, в играх с физикой их часто разделяют.
  • Библиотека (SFML, raylib, SDL) — набор функций для окна, ввода, графики и звука. Структуру игры пишете вы.
  • Движок (Unreal Engine, Godot) — готовая среда с редактором сцен, физикой, импортом ресурсов и сборкой под платформы. Вы пишете код внутри его правил.

По языку достаточно базы: переменные, условия, циклы, функции, std::vector и std::string, структуры или классы. Указатели и шаблоны на первой игре почти не понадобятся.

Как выбрать путь: консоль, библиотека или движок

Путь Что дает Когда брать Ограничение
Консоль, только стандартная библиотека Логика и цикл без установки чего-либо Первая игра, понять устройство цикла Нет окна и графики, ввод ждет Enter
raylib Окно, 2D, простое 3D, звук; API на C, работает из C++ Самый короткий путь от консоли к окну Меньше готовой архитектуры, собирать структуру самому
SFML Объектный API на C++: окно, 2D-графика, звук, сеть Хочется писать 2D в стиле C++ с классами Ветка SFML 3 требует C++17, туториалы по 2.x частично устарели
SDL Низкоуровневый кроссплатформенный слой: окно, ввод, звук, 2D-рендер Нужен контроль и перенос на много платформ Больше ручной работы, API на C; актуальная ветка SDL3 (с 2025) несовместима с SDL2, туториалы по SDL2 переносятся с правками
Unreal Engine 5 Полный движок, геймплей на C++ и Blueprints Цель — 3D и работа в студии на Unreal Тяжелый старт, сначала учите движок, потом свой код

Практическое правило для новичка: первая игра в консоли, вторая — та же логика на raylib или SFML, движок — когда понятно, что именно он делает за вас. Это не единственный порядок: если цель — устроиться в студию на Unreal, логично раньше перейти к движку. Сравнение C++ с другими языками для игр — в статье о языках программирования игр.

Первая игра на C++ целиком: сокровища в подземелье

Правила: игрок @ ходит по карте командами w a s d, собирает две монеты $, после этого открывается выход E. Стены # не пускают, q завершает игру. Игра пошаговая: одна строка ввода — один ход.

#include <iostream>
#include <string>
#include <vector>

struct Game {
    std::vector<std::string> map = {
        "#######",
        "#@.#.$#",
        "#.$#.E#",
        "#.....#",
        "#######",
    };
    int px = 1, py = 1;     // позиция игрока @
    int coins_left = 2;     // монеты $, без них выход E закрыт
    int moves = 0;
    bool running = true;
    std::string message;

    void update(char cmd) {                  // правила игры
        message.clear();
        int dx = 0, dy = 0;
        switch (cmd) {
            case 'w': dy = -1; break;
            case 's': dy = 1;  break;
            case 'a': dx = -1; break;
            case 'd': dx = 1;  break;
            case 'q': running = false; message = "выход"; return;
            default:  message = "команды: w a s d q"; return;
        }
        char& target = map[py + dy][px + dx];
        if (target == '#') { message = "стена"; return; }
        if (target == 'E' && coins_left > 0) { message = "выход закрыт"; return; }
        if (target == '$') { --coins_left; message = "монета"; }
        if (target == 'E') { running = false; message = "победа"; }
        map[py][px] = '.';
        px += dx;
        py += dy;
        target = '@';
        ++moves;
    }

    void render() const {                    // вывод состояния
        for (const auto& row : map) std::cout << row << '\n';
        std::cout << "ходов: " << moves << ", монет: " << coins_left;
        if (!message.empty()) std::cout << " | " << message;
        std::cout << "\n\n";
    }
};

int main() {
    Game game;
    game.render();
    std::string line;
    while (game.running && std::getline(std::cin, line)) {  // 1. ввод
        game.update(line.empty() ? ' ' : line[0]);           // 2. обновление
        game.render();                                       // 3. отрисовка
    }
    std::cout << "итог: " << (game.running ? "ввод закончился" : game.message)
              << ", ходов " << game.moves << '\n';
}

Сборка и запуск: g++ -std=c++17 -Wall -Wextra game.cpp -o game, затем ./game (в Windows — game.exe). Чтобы не вводить ходы руками, их можно подать через конвейер. Команды d, d, q дают такой хвост вывода: первый шаг вправо проходит, второй упирается в стену, счетчик ходов не растет.

#######
#.@#.$#
#.$#.E#
#.....#
#######
ходов: 1, монет: 2 | стена

#######
#.@#.$#
#.$#.E#
#.....#
#######
ходов: 1, монет: 2 | выход

итог: выход, ходов 1

Маршрут s d s d d w w d s собирает обе монеты и заканчивается строкой итог: победа, ходов 9. Если же идти к E через нижний коридор с одной монетой, игра ответит выход закрыт и не засчитает ход.

Разбор: где здесь игровой цикл

  • Состояние хранится в одной структуре Game: карта, позиция, счетчики. Это то, что меняется от хода к ходу.
  • update — правила. Функция ничего не печатает, она только меняет состояние. Проверка стены стоит до перемещения, поэтому игрок не может оказаться внутри #; выход за пределы массива исключен тем, что карта обнесена стеной.
  • render — только вывод. Он ничего не решает, поэтому его можно заменить отрисовкой в окне, не трогая правила.
  • Цикл в main повторяет «ввод -> обновление -> отрисовка», пока running истинно и ввод не закончился.

Разделение update и render — главная идея статьи. Именно оно позволяет перенести игру из консоли в окно: меняется источник ввода и способ рисования, правила остаются.

У этой игры есть граница: std::getline ждет, пока игрок нажмет Enter. Для пошаговой игры это нормально, для «Змейки» или платформера — нет. Стандартный C++ не умеет читать нажатие клавиши без ожидания: старые консольные «Змейки» используют conio.h из Windows, в Linux и macOS для этого берут ncurses, но проще сразу перейти к библиотеке с окном.

Игра в реальном времени: цикл с фиксированным шагом

Когда цикл не ждет ввода, кадры идут с разной длительностью: 16 мс, потом 50 мс из-за нагрузки, потом 4 мс. Если двигать объект «на 2 пикселя за кадр», на быстром компьютере он поедет быстрее. Классическое решение — копить реальное время и обновлять логику ровными шагами.

#include <iostream>

int main() {
    const double step = 1.0 / 60.0;          // шаг логики: 60 обновлений в секунду
    const double speed = 120.0;              // скорость объекта, пикселей в секунду
    const double frames[] = {0.016, 0.050, 0.004, 0.016};  // сколько длился каждый кадр, с
    double accumulator = 0.0, x = 0.0;

    for (double frame_time : frames) {       // так выглядит один оборот игрового цикла
        accumulator += frame_time;
        int updates = 0;
        while (accumulator >= step) {        // логика догоняет реальное время
            x += speed * step;
            accumulator -= step;
            ++updates;
        }
        std::cout << "кадр " << frame_time * 1000 << " мс: обновлений " << updates
                  << ", x = " << x << '\n';
    }
}

Длительности кадров здесь заданы массивом, чтобы вывод был воспроизводимым; в настоящей игре их измеряют через std::chrono::steady_clock или функцию библиотеки.

кадр 16 мс: обновлений 0, x = 0
кадр 50 мс: обновлений 3, x = 6
кадр 4 мс: обновлений 1, x = 8
кадр 16 мс: обновлений 1, x = 10

За 86 мс прошло 5 шагов по 1/60 секунды, объект сдвинулся на 10 пикселей, как и положено при 120 пикселях в секунду, независимо от того, как нарезаны кадры. Первый кадр короче шага, поэтому обновлений 0. Для простого движения без физики часто хватает варианта проще — умножать скорость на длительность кадра dt; фиксированный шаг нужен, когда важна одинаковая физика и столкновения при любом FPS.

Та же идея в окне: минимальный цикл на raylib

Так выглядит игровой цикл в библиотеке: круг в окне двигается стрелками, скорость задана в пикселях в секунду и умножается на время кадра.

#include "raylib.h"

int main() {
    InitWindow(800, 450, "First game");
    SetTargetFPS(60);
    Vector2 pos = {400.0f, 225.0f};

    while (!WindowShouldClose()) {           // окно закрыто или нажат Esc
        float dt = GetFrameTime();           // секунды с прошлого кадра
        if (IsKeyDown(KEY_RIGHT)) pos.x += 200.0f * dt;
        if (IsKeyDown(KEY_LEFT))  pos.x -= 200.0f * dt;

        BeginDrawing();
        ClearBackground(RAYWHITE);
        DrawCircleV(pos, 20.0f, RED);
        EndDrawing();
    }
    CloseWindow();
}

Ожидаемый результат — окно 800×450 с красным кругом, который плавно едет влево и вправо. Сравните с консольной игрой: IsKeyDown заменил std::getline, блок между BeginDrawing и EndDrawing заменил render. Чтобы перенести подземелье в окно, достаточно рисовать клетки карты прямоугольниками вместо символов.

План: от первой строки до игры в окне

  1. Соберите и запустите консольную игру выше, поменяйте карту и добавьте третью монету.
  2. Вынесите карту в текстовый файл и читайте ее через std::ifstream — так появятся уровни.
  3. Сохраняйте лучший результат (меньше ходов) в файл между запусками.
  4. Установите raylib или SFML и запустите пример с кругом. Удобнее через CMake и пакетный менеджер (vcpkg, Conan, Homebrew, apt), чем вручную.
  5. Перенесите Game в окно: ввод с клавиатуры, отрисовка клеток, затем плавное движение с dt.
  6. Добавьте состояния «меню -> игра -> пауза -> конец», звук и простые столкновения.
  7. Соберите релизную версию и дайте поиграть другому человеку на его компьютере — это и есть готовый артефакт.

Срок зависит от опыта: консольную версию обычно делают за вечер, перенос в окно занимает больше из-за настройки сборки.

Если не получилось: частые симптомы

Симптом Причина Что сделать
fatal error: 'SDL2/SDL.h' file not found Компилятор не знает, где заголовки библиотеки Передать путь -I, а лучше подключить библиотеку через CMake и пакетный менеджер; для SDL3 заголовок другой — SDL3/SDL.h
Undefined symbols for architecture arm64 (clang, macOS) или undefined reference to (GCC) Заголовок найден, но библиотека не подключена при линковке Добавить -lSDL2 (для SDL3 — -lSDL3), -lraylib или target_link_libraries в CMake
Кириллица в консоли Windows выводится неверно Кодировка консоли не совпадает с кодировкой исходника Сохранить файл в UTF-8, в MSVC собирать с флагом /utf-8, переключить консоль на UTF-8 (chcp 65001) или выводить латиницей
Объект на одном компьютере быстрее, чем на другом Скорость задана «за кадр» Умножать на dt или обновлять логику фиксированным шагом
Персонаж проходит сквозь стены Позиция меняется до проверки клетки Сначала проверить цель, потом двигать, как в update выше

Первые две строки — реальный вывод Apple clang 21 для программы с #include <SDL2/SDL.h>: без -I к каталогу заголовков и без -lSDL2. Формулировки GCC и MSVC отличаются, но смысл тот же: первая ошибка — на этапе компиляции, вторая — на этапе линковки.

Выводы

  • Игра на C++ — это логика на языке, игровой цикл «ввод -> обновление -> отрисовка» и библиотека или движок для окна, ввода и звука.
  • Первую игру разумно написать в консоли на стандартной библиотеке: так видно устройство цикла без настройки сборки.
  • Разделение update (правила) и render (вывод) позволяет перенести ту же логику в окно на raylib, SFML или SDL.
  • В реальном времени скорость задают в единицах за секунду и обновляют логику по dt или фиксированным шагом, иначе игра зависит от FPS.
  • Ошибки «файл не найден» и «неопределенный символ» — это разные этапы: путь к заголовкам и подключение библиотеки.

Где применяется / связь с практикой

Освойте тему на практике

Игровой цикл, разделение состояния и отрисовки, работа с файлами и сборкой через CMake — общие навыки C++, которые нужны и в геймдеве, и в системной разработке. Если хочется пройти базу языка системно, с практикой и разбором кода, посмотрите базовый курс по C++ с нуля. Разобрать отдельные темы с практиками можно на открытых уроках Otus.

FAQ

Можно ли написать игру на C++ без библиотек?
Консольную пошаговую игру — да, как в примере выше. Для окна, графики и звука все равно нужен интерфейс операционной системы (WinAPI, X11/Wayland, Cocoa), поэтому на практике берут кроссплатформенную библиотеку.

Какую версию стандарта C++ ставить?
Для примеров из статьи хватает C++17. Он же нужен для SFML 3; более новые стандарты C++20 и C++23 тоже подходят, если компилятор их поддерживает.

C или C++ для первой игры?
raylib и SDL написаны на C и работают из обоих языков. C++ удобнее за счет std::vector, std::string и классов, поэтому для игры с состоянием и уровнями он проще в сопровождении.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)