Разработка игрового движка на C99


Введение

Очень часто разработчикам, так или иначе связанным с разработкой игр, в голову приходит идея создать собственный игровой движок. Я приступил к разработке с целью разобраться в устройстве и архитектуре современных движков. В основе я хотел использовать подход 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, рендеринг моделей и многое другое. С проектом можно ознакомиться по ссылке.




К сожалению, не доступен сервер mySQL