Введение
Очень часто разработчикам, так или иначе связанным с разработкой игр, в голову приходит идея создать собственный игровой движок. Я приступил к разработке с целью разобраться в устройстве и архитектуре современных движков. В основе я хотел использовать подход ECS, в связи с чем решил отказаться от использования ООП вообще за ненадобностью. Выбор пал на язык C стандарта 99 года, так как он наиболее совместим с большинством устройств. Реализовывать сам ECS фреймворк цели не было, но возможно в будущем я займусь и этим, поэтому в основу лёг фреймворк flecs.
Архитектура
В качестве системы сборки был выбран cmake. Я хотел сделать движок модульным и масштабируемым, поэтому создал в cmake список модулей (set(MODULES core physics graphics
), в каждом из которых раздельно хранились заголовочные файлы и файлы с кодом в директориях src
и include
соответственно. Сборка этих модулей происходит следующим способом:
foreach(MODULE ${MODULES})
file(GLOB MODULE_SRC ${MODULE}/src/*.c)
set(MODULE_INCLUDES ${MODULE}/include/)
include_directories(${MODULE_INCLUDES})
add_library(${MODULE} ${_SHARED} ${MODULE_SRC})
endforeach()
Таким образом, чтобы добавить новый модуль, достаточно лишь включить соответствующую директорию в список MODULES
. Подробнее конфиг для cmake можно изучить по ссылке. Также в точке входа программы, функции main
, нужно проинициализировать окно SDL, контекст OpenGL в нем, мир для ECS, встроенные системы и компоненты. После этого можно запустить основной цикл для мира ECS.
Таким образом `main.c` представляет из себя следующее:
// Внешние библиотеки
#include <stdio.h>
#include <log.h>
#include <flecs.h>
#include <nuklear_include.h>
#include <graphics_components.h>
// Внутренние модули движка
#include "window.h"
#include "events.h"
#include "globals.h"
#include "light.h"
#include "game.h"
#include "stages.h"
#include "core_components.h"
#include "simple_meshes.h"
#include "config.h"
#include "models.h"
int main() {
int res;
world = ecs_init(); // Инициализация "мира" ECS
if (world == NULL) {
log_fatal("Can't init flecs world");
return -1;
}
res = create_window(); // Функция для создания окна SDL (будет рассмотрено позже)
if (res != 0) {
log_fatal("Can't create SDL window");
ecs_fini(world);
return -1;
}
nk_ctx = nk_sdl_init(window);// инициализация контекста для nuklear (будет рассмотрено позже)
if (nk_ctx == NULL) {
log_fatal("Can't init nuklear context");
destroy_window();
ecs_fini(world);
return -1;
}
nk_sdl_font_stash_begin(&atlas);
nk_sdl_font_stash_end();
init_stages(); // Инициализация этапов конвейера для ECS
init_core_components(); // Инициализация компонентов из модуля core
init_graphics_components(); // Инициализация компонентов из модуля graphics
init_simple_meshes(); // Инициализация компонентов и систем,
init_models(); // которые будут рассмотрены позже.
init_lights(); //
init_graphics(); //
ECS_TAG_DEFINE(world, global_tag); // Данный тэг объявлен для тех систем,
// которые работают с глобальным контекстом,
// а не с конкретными сущностями.
ECS_ENTITY(world, global_entity, global_tag);
// Следующие три системы выполняются в глобальном контексте на этапах
// перед отрисовкой окна, после отрисовки окна и на этапе обработки событий
ECS_SYSTEM(world, pre_render_window_system, pre_render_stage, global_tag);
ECS_SYSTEM(world, post_render_window_system, post_render_stage, global_tag);
ECS_SYSTEM(world, process_events_system, events_stage, global_tag);
// Инициализация игры
res = init_game();
#ifdef CONFIG_FPS
ecs_set_target_fps(world, CONFIG_FPS);
#endif
if (res != 0) {
nk_sdl_shutdown();
destroy_window();
ecs_fini(world);
log_fatal("Can't init game");
return -1;
}
// Главный цикл
while (!quit_flag) {
ecs_progress(world, 0);
}
// Деинициализация
nk_sdl_shutdown();
destroy_window();
ecs_fini(world);
return 0;
}
Для переменных, доступных глобально, я объявляю их отдельно в модуле core
в файле globals.c
и создаю с помощью ключевого слова extern
их объявления в заголовочном файле globals.h
, теперь их можно использовать везде, достаточно лишь включить заголовочный файл. Как вариант можно было создать entity для глобального контекста с глобальными переменными в качестве компонентов, но тогда везде, где необходимо окно или контекст opengl, пришлось бы делать query к entity с глобальным контекстом, что усложнило бы в разы код.
Заключение
В следующих статьях я подробнее распишу создание и обработку окна SDL, рендеринг моделей и многое другое. С проектом можно ознакомиться по ссылке.