Окно и главный цикл
Откроешь окно GLFW и напишешь главный цикл, который держит окно на экране, пока окно не закроют крестиком.
Откроем наш проект в Visual Studio. GLFW уже собрана и подключена, но app пользуется библиотекой только ради одной строки в консоли и сразу завершается, как обычная консольная утилита. Движку нужна программа другого рода: окно, которое остаётся на экране, пока пользователь не закроет окно крестиком, и в котором потом видеокарта будет рисовать картинку.
Запустить GLFW
Почти любую функцию GLFW можно вызывать только после glfwInit. Функция glfwInit готовит GLFW к работе: glfwInit загружает библиотеки Windows (файлы .dll), через которые GLFW работает с окнами, геймпадами и масштабом экрана, находит подключённые мониторы и создаёт невидимое служебное окно, через которое GLFW узнаёт о подключении мониторов и геймпадов. Если что-то из этого не получилось, glfwInit возвращает 0.glfwInit — функция glfwTerminate: она закрывает окна, которые программа не закрыла сама, и отдаёт Windows всё, что glfwInit взяла. Поэтому glfwTerminate вызывают перед тем, как программа завершится.
GLFW сообщает об ошибке не исключением и не текстом в консоли: GLFW вызывает функцию, которую ты передал заранее. Такую функцию называют функцией обратного вызова (по-английски callback): ты отдаёшь функцию библиотеке, а библиотека сама вызывает функцию, когда нужно. Функцию для ошибок программа передаёт в GLFW через glfwSetErrorCallback («задать функцию для ошибок»).
Открой main.cpp в корне проекта, D:\ Эта строка стояла в main до этой главы, и в новом коде строки нет: std::cout << std::format("GLFW {}\n", glfwGetVersionString());
Первая новая строка, glfwSetErrorCallback, передаёт GLFW функцию для ошибок. Сама функция записана прямо в скобках, без имени. Такая функция без имени называется [] значат, что лямбда не берёт ничего из main, а работает только со своими параметрами. В круглых скобках параметры, как у обычной функции, в фигурных — тело.code и описание ошибки description.
Лямбда печатает строку вида Например, такую строку напечатала бы лямбда, если бы программа вызвала функцию GLFW раньше, чем Число 65537 — это GLFW error 65537: …glfwInit:GLFW error 65537: The GLFW library is not initialized0x00010001, номер ошибки GLFW_NOT_INITIALIZED («GLFW не запущена»). Номера ошибок записаны в заголовке GLFW D:\std::cerr, а не через std::cout. std::cerr — std::cout, и выход для ошибок, std::cerr. В консоли отладки Visual Studio текст из обоих выходов выглядит одинаково. Разница видна, когда программу запускают из консоли и пишут вывод в файл, например app.exe > log.txt: в файл уходит только текст из std::cout, а ошибки из std::cerr остаются на экране. Ещё std::cerr печатает строку сразу, а std::cout может копить текст и печатать кусками: если программа упадёт, строка об ошибке из std::cerr успеет появиться.
Функцию для ошибок задают первой, ещё до glfwInit: тогда, если не получится сама glfwInit, функция для ошибок тоже напечатает причину. Если glfwInit вернула 0, программа выходит с
Запусти программу Контурный зелёный треугольник в верхней полосе окна Visual Studio, правее списка app.exe. Кнопка запускает программу без отладчика, как и Ctrl+F5:
D:\dev\MyEngine\out\build\vs2026\Debug\app.exe (process 14220) exited with code 0 (0x0).Press any key to close this window . . .Код возврата 0 значит, что программа дошла до строки return 0. Значит, проверка if (!glfwInit()) не сработала: glfwInit вернула 1, и GLFW готова к работе.
#include <format>#include <iostream>#include <GLFW/glfw3.h>int main(){ glfwSetErrorCallback([](int code, const char* description) { std::cerr << std::format("GLFW error {}: {}\n", code, description); }); if (!glfwInit()) { return 1; } glfwTerminate(); return 0;}В новом коде две непривычные вещи: функция без имени, записанная прямо в скобках, и библиотека, которая сама вызывает твой код. Схема ниже разбирает по шагам обе вещи: как обычная функция становится лямбдой и как GLFW находит лямбду, когда случается ошибка.
Как работает callback в C++ на примере glfwSetErrorCallback
PrintError стоит над main, а строка внутри main отдаёт эту функцию в GLFW. Имя PrintError нужно здесь ровно один раз: чтобы сослаться на функцию в этой строке.[]. Получилась лямбда. Это та же самая функция с теми же параметрами и тем же телом, только записанная на месте.Переменная объявлена в файле D:\GLFWerrorfun значит «адрес функции для ошибок»:
static GLFWerrorfun _glfwErrorCallback;_glfwErrorCallback, и сначала в этой переменной пусто. Строка с glfwSetErrorCallback записывает в переменную адрес лямбды: это стрелка 1. Сама лямбда при этом не выполняется, GLFW только запомнила адрес.glfwInit: это стрелка 2. Допустим, внутри что-то не получилось. Тогда GLFW заглядывает в свою переменную _glfwErrorCallback и смотрит, записан ли в ней адрес лямбды: это стрелка 3. Если там пусто, GLFW ничего не вызывает, а если адрес есть, он будет использован на следующем шаге.Эти две строки стоят в конце функции _glfwInputError, через которую проходит каждая ошибка GLFW, в файле D:\
if (_glfwErrorCallback) _glfwErrorCallback(code, description);_glfwErrorCallback(code, description);: строка берёт адрес из переменной и переходит по нему в лямбду. Это стрелка 4, момент, когда программа выходит из кода GLFW и попадает в функцию, которую написал ты. Строка передаёт лямбде два значения: номер ошибки попадает в параметр code, а описание ошибки попадает в параметр description.code и description. Тело лямбды печатает, и строка об ошибке попадает в консоль: это стрелка 5. Лямбда работает как любая функция: когда функция заканчивается, программа продолжает работу с той строки, которая функцию вызвала. Лямбду вызвала строка в GLFW, поэтому программа возвращается в GLFW: это стрелка 6.glfwInit возвращает 0 в твою программу: это стрелка 7. Проверка в main срабатывает, и программа выходит с кодом 1.main: ни одна не вызывает лямбду. Программа только передала адрес и пошла дальше. Единственный вызов лямбды стоит в коде GLFW и выполняется, только когда случилась ошибка. Программа заранее не знает, будет ли ошибка и в какой функции GLFW, а GLFW узнаёт об ошибке первой. Так работает любая функция обратного вызова: что сделать, пишешь ты, а вызывает этот код библиотека, когда наступает нужное событие.Открыть окно
GLFW работает, и теперь программа откроет окно. Окно создаёт функция glfwCreateWindow, а убирает функция glfwDestroyWindow.
Допиши в main.cpp строки, отмеченные в карточке рядом: создание окна с проверкой после glfwInit и glfwDestroyWindow перед glfwTerminate в конце.
Размер рабочей области окна, куда программа рисует: 1280 пикселей по ширине и 720 по высоте. Рамку и полосу заголовка Windows добавляет снаружи, поэтому всё окно получается чуть больше. Если в настройках экрана Windows стоит масштаб 150%, привычные программы, например проводник, увеличены в полтора раза. Окно GLFW так не увеличивается: оно занимает ровно 1280 на 720 настоящих пикселей экрана, поэтому на таком экране выглядит меньше, чем ты ждёшь. Это не ошибка, а обычное поведение GLFW.glfwCreateWindow принимает пять значений: nullptr значит «обычное окно с рамкой».nullptr.
Функция возвращает В заголовке GLFW тип Всё, из чего состоит окно, лежит в памяти GLFW: размер окна, заголовок, отметка «окно просят закрыть» и настоящее окно Windows. Твоя программа видит только указатель на этот набор данных и не может залезть внутрь. Указатель похож на номерок в гардеробе: по номерку выдают пальто, но сам номерок пальто не содержит.GLFWwindow* — GLFWwindow только назван, а что лежит внутри, не написано:typedef struct GLFWwindow GLFWwindow;
Если окно создать не удалось, glfwCreateWindow возвращает nullptr, а лямбда печатает причину. Тогда программа выключает GLFW через glfwTerminate и выходит с кодом 1. В проверке if (!glfwInit()) вызова glfwTerminate нет, потому что неудачная glfwInit сама убирает всё, что успела создать. А здесь glfwInit уже сработала, и выключить GLFW должна программа.
glfwDestroyWindow уничтожает окно, то есть просит Windows закрыть окно навсегда, и освобождает всё, что GLFW создала для этого окна.
Запусти программу без отладки (Ctrl+F5). Окно мелькнёт на долю секунды, а может, ты окно даже не заметишь, и в консоли снова будет строка exited with code 0.
#include <format>#include <iostream>#include <GLFW/glfw3.h>int main(){ glfwSetErrorCallback([](int code, const char* description) { std::cerr << std::format("GLFW error {}: {}\n", code, description); }); if (!glfwInit()) { return 1; } GLFWwindow* window = glfwCreateWindow(1280, 720, "Engine", nullptr, nullptr); if (!window) { glfwTerminate(); return 1; } glfwDestroyWindow(window); glfwTerminate(); return 0;}Окно мелькнуло и сразу пропало. Схема ниже показывает почему: в каком порядке программа выполняет вызовы GLFW и сколько времени окно существует между этими вызовами.
Почему окно GLFW сразу закрывается
glfwInit включает GLFW. С этого вызова GLFW работает, и можно вызывать почти любую другую функцию GLFW. На экране пока ничего нет.glfwCreateWindow создаёт окно, и окно появляется на экране.glfwDestroyWindow: в коде После glfwCreateWindow стоит только проверка, что окно создалось. Если окно создалось, проверка ничего не делает, и сразу следующей строкой идёт glfwDestroyWindow:
GLFWwindow* window = glfwCreateWindow(1280, 720, "Engine", nullptr, nullptr); if (!window) { glfwTerminate(); return 1; } glfwDestroyWindow(window);glfwTerminate выключает GLFW. Время жизни окна лежит целиком внутри времени работы GLFW, поэтому при выходе порядок обратный запуску: сначала программа уничтожает окно, потом выключает GLFW. Чтобы окно осталось на экране, программе нужно задержаться между созданием окна и уничтожением окна.Главный цикл
Чтобы окно оставалось на экране, программа не должна доходить до glfwDestroyWindow, пока окно не закрыли. Для этого в движке есть while, который крутится между созданием окна и glfwDestroyWindow.
Допиши цикл между проверкой if (!window) и строкой glfwDestroyWindow. Новые строки в карточке рядом отмечены.
Цикл while снова и снова выполняет строки в фигурных скобках, пока условие в круглых скобках верно. Здесь в фигурных скобках одна строка, glfwPollEvents, а в условии стоит вызов glfwWindowShouldClose.
glfwPollEvents на каждой
glfwWindowShouldClose(window) окно не закрывает, а только отвечает на вопрос «просили ли закрыть это окно?». Ответ «да» появляется, когда glfwPollEvents увидела щелчок по крестику. Перед вызовом стоит !, который переворачивает ответ. Пока крестик не нажали, ответ «нет», с ! условие верно, и цикл крутится дальше. После щелчка по крестику ответ «да», с ! условие неверно, цикл заканчивается, и программа идёт к glfwDestroyWindow.
Запусти программу без отладки (Ctrl+F5). Теперь окно остаётся на экране: окно можно перетащить за заголовок, растянуть за край и свернуть. Закрой окно крестиком, и в консоли появится строка exited with code 0.
Внутри окна пока пусто: программа ещё ничего не рисует. Зато окно двигается, меняет размер и закрывается крестиком, и всё это делают две строки цикла: glfwPollEvents и условие с glfwWindowShouldClose.
GLFWwindow* window = glfwCreateWindow(1280, 720, "Engine", nullptr, nullptr); if (!window) { glfwTerminate(); return 1; } while (!glfwWindowShouldClose(window)) { glfwPollEvents(); } glfwDestroyWindow(window); glfwTerminate(); return 0;}Схема ниже показывает, как две строки цикла управляют окном: откуда берутся движения мыши и щелчок по крестику и как щелчок заканчивает цикл.
Как окно получает события: зачем нужен glfwPollEvents
glfwPollEvents («опросить события»). Функция забирает из очереди событий всё, что накопилось, и разбирает каждое событие. Положение мыши GLFW запоминает Окно GLFW — структура в памяти программы: в ней лежат все данные окна. Среди данных окна два поля, то есть две переменные внутри структуры, с положением мыши: virtualCursorPosX и virtualCursorPosY. Функция glfwPollEvents записывает в эти поля новое положение при каждом событии «мышь сдвинулась».
Когда программа вызывает glfwGetCursorPos, GLFW не спрашивает Windows, а просто отдаёт значения этих полей. Поля описаны в файле D:\
DefWindowProc («обработка окна по умолчанию»). Эта функция и двигает окно, меняет размер и рисует рамку. Но вызывает функцию программа, внутри glfwPollEvents: сама Windows этого не делает.glfwPollEvents. Если программа перестанет забирать события, очередь будет расти, окно перестанет двигаться, и через несколько секунд Windows допишет в заголовок окна «(Не отвечает)».glfwPollEvents забирает это событие, и GLFW ставит окну флаг «закрыть» со значением «да». Флаг — В файле D:\shouldClose («нужно закрыть») значение GLFW_TRUE, то есть 1:
void _glfwInputWindowCloseRequest(_GLFWwindow* window){ assert(window != NULL); window->shouldClose = GLFW_TRUE;glfwWindowShouldClose («должно ли окно закрыться») читает флаг «закрыть» и возвращает 1, если флаг стоит, и 0, если нет. Пока флаг «нет», цикл начинает новую итерацию. Когда флаг «да», цикл заканчивается, и программа идёт к строкам после цикла.glfwDestroyWindow уничтожает окно: Windows закрывает окно навсегда, и окна больше нет. Затем glfwTerminate выключает GLFW, строка return 0 отдаёт exited with code 0.На схеме цикл замедлен, чтобы было видно каждый шаг. Настоящий цикл ничего не ждёт: как только glfwPollEvents разобрала очередь, цикл сразу начинает новую итерацию, даже если событий не было. Поэтому пустое окно целиком занимает один
Итог
Программа app запускает GLFW, открывает окно 1280 на 720 и крутит главный цикл: каждая итерация забирает события окна, а после крестика выходит из цикла, убирает окно и выключает GLFW.
Так выглядит main.cpp после главы. CMakeLists.txt в этой главе не менялся.