Создание игры на C++ сводится к трем вещам: логика игры на самом языке, игровой цикл (game loop), который раз за разом читает ввод, обновляет состояние и рисует кадр, и библиотека или движок, которые дают окно, клавиатуру, графику и звук. Стандартная библиотека C++ окон и графики не умеет, поэтому первую игру удобно написать в консоли, а затем перенести ту же логику в окно на SFML, raylib или SDL.
Содержание
- Что нужно знать до начала: мини-словарь
- Как выбрать путь: консоль, библиотека или движок
- Первая игра на C++ целиком: сокровища в подземелье
- Разбор: где здесь игровой цикл
- Игра в реальном времени: цикл с фиксированным шагом
- Та же идея в окне: минимальный цикл на raylib
- План: от первой строки до игры в окне
- Если не получилось: частые симптомы
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — словарь, выбор инструмента, рабочая консольная игра целиком, разбор цикла с фиксированным шагом, план до игры в окне и частые ошибки сборки. Консольные примеры собраны и запущены 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. Чтобы перенести подземелье в окно, достаточно рисовать клетки карты прямоугольниками вместо символов.
План: от первой строки до игры в окне
- Соберите и запустите консольную игру выше, поменяйте карту и добавьте третью монету.
- Вынесите карту в текстовый файл и читайте ее через
std::ifstream— так появятся уровни. - Сохраняйте лучший результат (меньше ходов) в файл между запусками.
- Установите raylib или SFML и запустите пример с кругом. Удобнее через CMake и пакетный менеджер (vcpkg, Conan, Homebrew, apt), чем вручную.
- Перенесите
Gameв окно: ввод с клавиатуры, отрисовка клеток, затем плавное движение сdt. - Добавьте состояния «меню -> игра -> пауза -> конец», звук и простые столкновения.
- Соберите релизную версию и дайте поиграть другому человеку на его компьютере — это и есть готовый артефакт.
Срок зависит от опыта: консольную версию обычно делают за вечер, перенос в окно занимает больше из-за настройки сборки.
Если не получилось: частые симптомы
| Симптом | Причина | Что сделать |
|---|---|---|
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 и классов, поэтому для игры с состоянием и уровнями он проще в сопровождении.



