Я занимаюсь разработкой игр больше десяти лет. За это время выпускал мобильные и веб-игры и в командах, и соло. В разное время использовал libGDX, внутренние движки (Lua и C++) и Defold. Последние 6+ лет регулярно выпускаю игры на Defold.

Я люблю минималистичные инструменты вроде Defold, raylib и libGDX. Они дают базовые блоки, из которых я могу собрать игру так, как мне нужно.

За годы работы с Defold я написал много кода поверх движка. В какой-то момент появилась шутливая мысль: “С таким количеством своего теха я бы уже мог написать свой движок”. Вначале это была просто шутка, но со временем своего теха становилось всё больше, и эта мысль всё меньше походила на шутку.

Ещё мне хотелось глубже разобраться в рендеринге и графических API. Готовые движки обычно скрывают большую часть этого слоя за своим API.

9 марта 2026 года я создал репозиторий Neotolis Engine и сделал первый коммит.

Небольшой дисклеймер про терминологию. Есть привычное разделение на игровые движки и фреймворки, и по текущему устройству Neotolis Engine во многом ближе к фреймворку. Но граница между ними довольно размыта и холиварна. Для себя я не провожу её строго и дальше буду называть Neotolis Engine просто движком.

Движок я делаю для себя и своих игр. Код открыт под MIT, пользоваться им может кто угодно, но развивается движок в первую очередь под мои проекты.

Neotolis Engine написан на C17, собирается в WebAssembly и использует WebGL 2 в браузере и OpenGL 3.3 на десктопе.

Я долго выбирал между WebGL 2 и WebGPU: по Poki Player Device Report на 19 августа 2026 года WebGPU доступен у 89,54% игроков (+7,23% Compatibility Mode), но в issue trackers много проблем на отдельных устройствах и браузерах. Поэтому сейчас выбрал WebGL 2, хотя через год-два, скорее всего, придётся писать WebGPU backend.

Я не делаю движок ради движка. Я разработчик игр, и я делаю инструмент, на котором буду выпускать свои игры.

Примерно через четыре месяца после начала разработки в Neotolis Engine уже было достаточно фич, чтобы перейти от технических демо к разработке игры. У меня был 2D-прототип Not a Trolley Problem на PixiJS, сделанный за 13 дней для Gamedev.js Jam. Я давно хотел развить эту идею в 3D, и 16 июля начал делать новую версию уже на своём движке.

За 30 дней я довёл новую 3D-версию до демо и показал её игрокам на Comic Con Tashkent(15–16 августа). Игра работает, игрокам нравится, и я доволен тем, что получилось. Впереди ещё много работы и над самой игрой, и над движком.

Вот так выглядит игра, сделанная на Neotolis Engine:

Каким я хотел сделать движок. 6 принципов

Перед началом разработки я сформулировал шесть принципов. Это моё видение движка и мой подход к работе: что для меня важно и что я ценю в инструментах и коде. Эти принципы определяют архитектуру Neotolis Engine.

1. Code-first

Для меня код это самый понятный и гибкий способ описывать игру. Вся логика перед глазами, и я легко могу менять порядок систем, включать и выключать их или добавлять новые.

Поэтому в Neotolis Engine нет обязательных GUI-настроек, конфигов, бинарных файлов с настройками или XML-разметки. Порядок систем, render pipeline, сборка ресурсов и UI задаются кодом. Если конкретной игре нужен конфиг или CLI-параметры, она может добавить их сама.

//cокращённый frame из **Not a Trolley Problem**
static void frame(void) {
    /* Input */
    nt_window_poll();
    nt_input_poll();
    game_input_capture(&s_input);

    /* Update */
    nt_resource_step();
    nt_material_step();
    game_runtime_host_update(
        &s_runtime,
        &s_input,
        s_cli.disable_autosave
    );

    /* Render */
    game_render_pipeline_begin_frame(&s_render_pipeline);
    game_render_pipeline_draw(
        &s_render_pipeline,
        &s_runtime.world,
        &s_runtime.features,
        &s_input,
        s_runtime.ready,
        game_scenes_should_render_world(&s_runtime.scenes)
    );
    nt_gfx_end_frame();

    /* Present */
    nt_window_swap_buffers();
}

int main(void) {
    /* initialization */
    nt_app_run(frame);
}

Мне близок олдскульный подход, где движок даёт update loop и базовые блоки, а что происходит внутри, как именно и в каком порядке решает разработчик.

2. Explicit over implicit

Мне важно, чтобы поведение движка было явным и предсказуемым. Когда он сам принимает решения за меня, всё хорошо до момента, когда мне нужно другое поведение. Тогда приходится искать, где это решение спрятано и как его обойти.

Если я делаю что-то неправильно, движок должен сразу сказать об этом ошибкой. Он не должен пытаться исправить ситуацию сам или принимать решение за меня.

Например, во многих движках уже есть готовая схема рендера: opaque, transparent и свои правила сортировки внутри. Её можно настраивать, но базовые решения уже приняты движком. В Neotolis Engine наоборот: игра сама определяет passes и порядок объектов в каждом из них.

Для каждого render item игра сама задаёт sort_key. В одном pass ключ можно строить по материалу, в другом по глубине, а где сортировка не нужна, вообще её не делать.

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

NT_ASSERT(desc->texture_count <= NT_MATERIAL_MAX_TEXTURES);

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

3. KISS: Keep It Simple, Stupid

Я стараюсь решать конкретные проблемы самым простым способом. Если решение можно сделать универсальным без дополнительной сложности, отлично. Если для универсальности приходится строить лишние слои и абстракции, я лучше решу текущую задачу напрямую.

При этом KISS для меня не означает писать всё самому. Небольшой модуль проще сделать самому, а для чего-то сложнее можно взять готовое решение. Поэтому для математики я использую cglm, для окна и ввода на десктопе GLFW, а для UI layout Clay.

Для меня главное не написать как можно меньше кода, а не тащить в проект сложность, которая мне не нужна.

В вебе у простоты есть ещё одно очень конкретное измерение: размер сборки.

4. Tiny size

Для веб-игр размер особенно важен. Poki рекомендует держать первую загрузку до 5 МБ, а всю игру примерно до 8 МБ.

Я отдельно слежу за размером runtime и ассетов. Размер WASM считается на каждом PR вместе с изменением относительно предыдущей версии. Если небольшая правка неожиданно добавит десятки килобайт или в runtime попадёт тяжёлая зависимость, я сразу это увижу.

Демо

WASM

Ассеты

Остальное

Итоговый размер

Hello

8.505 КБ

4.499 КБ

13.004 КБ gzip

UI Showcase

491.070 КБ

9.669 КБ

500.739 КБ gzip

Text Rendering

43.479 КБ

7.583 МБ

8.825 КБ

7.634 МБ gzip

Остальное: index.html и загрузочный JavaScript.

Это размеры конкретных демо-сборок, каждое демо подключает только нужные модули, а неиспользуемый код не попадает в итоговый WASM.

Text Rendering сильно выбивается. Весь объём занимает text_cjk.ntpack: в нём 39 238 китайских, японских и корейских глифов, 7.571 МБ gzip.

Размер ассетов я тоже стараюсь держать минимальным. Builder заранее обрабатывает их, а сами ассеты можно разделять на отдельные паки. Поэтому для запуска игры достаточно загрузить только необходимое, а музыку, CJK-глифы или HD-атласы можно догрузить позже.

5. Set of modules

Neotolis Engine состоит из отдельных модулей, и игра подключает только то, что ей нужно.

2D-игра может обойтись без mesh component и mesh renderer. Если игра не использует спрайты, ей не нужен sprite renderer. Если не нужна система ресурсов, её тоже можно не подключать.

Демо Bench Shapes собирается только из core, app, window, input, gfx, math, log и shape renderer. Системы ресурсов, UI, mesh renderer и большинства других модулей в этой сборке нет. При этом это 3D-сцена с примитивными shapes, по которой можно летать на WASD, Space/Shift и управлять камерой мышью.

Можно собрать headless-версию для тестов или сервера, заменив окно, ввод и графику пустыми stub-реализациями.

Инструменты для разработки тоже подключаются отдельно. Metrics, debug overlay и DevAPI можно использовать во время разработки и не включать в release.

6. Prebuilt assets

Вся тяжёлая работа с ассетами происходит на этапе сборки. В игру попадают уже подготовленные данные.

Runtime вообще не работает с исходными форматами вроде PNG, glTF или TTF. Все данные он получает уже в подготовленном виде, после обработки билдером.

Билдер заранее конвертирует меши, готовит шрифты, собирает атласы, сжимает текстуры через Basis Universal и складывает всё в .ntpack.

Правила сборки ассетов задаются обычным C-кодом. Например, вот сокращённый фрагмент билдера из Not a Trolley Problem:

NtBuilderContext *ctx =
    nt_builder_start_pack(pack_path(out_dir, "game.ntpack"));

/* Font: bake only glyphs used by the game */
/* LOC_CHARSET_NON_ASCII is generated from localization */
#define GAME_CHARSET NT_CHARSET_ASCII LOC_CHARSET_NON_ASCII

nt_builder_add_font(
    ctx,
    "assets/fonts/Rubik-Regular.ttf",
    &(nt_font_opts_t){
        .charset = GAME_CHARSET,
        .resource_name = "game/font_body",
    });

/* Texture: compress at build time */
const nt_tex_compress_opts_t compress =
    nt_tex_compress_etc1s_high();

nt_tex_opts_t tex_opts = nt_tex_opts_defaults();
tex_opts.compress = &compress;

nt_builder_add_texture(
    ctx,
    "assets/ui/currency/tram_token.png",
    &tex_opts);

/* Atlas: pack UI sprites and compress the page */
const nt_tex_compress_opts_t ui_compress =
    nt_tex_compress_etc1s_high();

nt_atlas_opts_t atlas_opts = nt_atlas_opts_defaults();
atlas_opts.compress = &ui_compress;

NtAtlasBuild *ui_atlas =
    nt_atlas_begin(ctx, "ui", &atlas_opts);

nt_atlas_sprite_opts_t tutorial_hand_opts =
    nt_atlas_sprite_opts_defaults();

tutorial_hand_opts.name = "tutorial_hand_tap";

nt_atlas_add(
    ui_atlas,
    "assets/ui/tutorial_hand_tap.png",
    &tutorial_hand_opts);

/* Build the atlas and add it to the pack */
nt_atlas_commit(ui_atlas);

nt_builder_finish_pack(ctx);
nt_builder_free_pack(ctx);

Для текстур доступны ETC1S и UASTC. Для шрифта задаётся конкретный charset, а atlas полностью собирается и сжимается ещё на build step.

Результат складывается в .ntpack. Это простой flat binary format: header, список assets, сами данные и необязательные metadata. После загрузки runtime валидирует pack и получает нужные данные напрямую по offset внутри загруженного блока.

Обратной совместимости у .ntpack намеренно нет. Runtime ожидает точную версию формата, а при несоответствии рантайм просто отказывается загружать пак.

Шесть принципов выше определяют, как устроен Neotolis Engine. Но собственный движок даёт ещё одну важную вещь: я сам выбираю технологии и могу добавлять их тогда, когда они мне нужны.

Свобода в выборе технологий. Добавляем Slug

С рендерингом текста мне просто повезло по времени. Я как раз дошёл до шрифтов и собирался делать их привычным способом через SDF.

17 марта 2026 года Eric Lengyel опубликовал reference shaders Slug. В SDF глиф заранее превращается в текстуру с полем расстояний. Slug вместо этого хранит векторный контур глифа из кривых и рендерит его прямо на GPU.

SDF проще и для большинства задач отлично работает. Но мне понравилось, что Slug сохраняет именно векторное представление шрифта. Поэтому я решил попробовать Slug вместо SDF.

31 марта в движке уже работал первый вариант Slug renderer, а 2 апреля появился text demo с несколькими языками. От публикации reference shaders до рабочего рендера в движке прошло 16 дней.

Как я использую ИИ в разработке

Отдельно хочу рассказать про ИИ. Весь код Neotolis Engine написан ИИ. Я не пишу код, я определяю, что и как должно работать, принимаю архитектурные решения, проектирую API, ставлю задачи и делаю ревью.

Чтобы был понятен масштаб проекта, сейчас исходники распределяются так:

Часть проекта

Файлов

Непустых строк

Runtime движка

224

46 694

Билдер ассетов

27

14 812

Тесты

154

64 296

Всего

405

125 802

Количество строк само по себе ничего не говорит о качестве кода, но показывает масштаб проекта и долю тестов. В подсчёт вошли исходники C, C++, JavaScript и TypeScript без зависимостей, примеров, сгенерированного кода и пустых строк.

Для больших задач я использую GSD (Get Shit Done), framework для spec-driven development для агентов. Он собирает контекст и требования, делает спеку, разбивает задачу на этапы, а затем проводит реализацию и проверку.

В основном код пишет Claude. Я пробовал использовать Codex, но мне не нравится, как он пишет. Появляется слишком много церемоний, дополнительных проверок и усложнений там, где можно было сделать проще. Claude обычно делает именно то, что я попросил.

Зато для review Codex очень хорош. Обычно Claude делает задачу, потом Codex проверяет код, Claude исправляет замечания, и я снова отправляю результат на review. Таких циклов может быть несколько.

После нескольких таких циклов я читаю pr сам. И всё равно регулярно нахожу странные решения. Код может работать и пройти несколько AI-review, но при этом быть просто нелогичным. Где-то слишком много слоёв, где-то простая вещь размазана на несколько функций, а где-то решение сделано слишком узко, хотя его легко сделать универсальным.

В тесты и CI я уже почти не лезу. Раньше читал и их, но код пишется очень быстро, ревью много, и на всё просто не хватает внимания. Поэтому решил тратить свой фокус на код движка, архитектуру и API.

CI полностью написал ИИ, и я этому рад. Я почти не знаю, как он там устроен. CI просто работает, запускает нужные сборки и проверки, а если ломается, сначала с этим разбираются агенты.

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

И здесь для ИИ особенно важен feedback loop: агент должен иметь возможность сделать изменение, запустить игру, проверить результат и при необходимости исправить его.

Для этого в движке есть DevAPI. Через него агент может управлять игрой, получать состояние, смотреть UI, объекты и логи, вызывать игровые функции. Если нужно проверить визуал, он может сделать скриншот или записать видео.

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

Код самой игры я почти не читаю. Я в основном смотрю на результат. Постоянно играю в билд, проверяю механики, UI, баланс. Если что-то работает странно или просто ощущается плохо, отправляю это обратно агенту на исправление.

Получается, что в движке я контролирую в первую очередь код и реализацию, а в игре поведение и то, как это выглядит.

Not a Trolley Problem. Игра за месяц.

16 июля я начал делать 3D-версию Not a Trolley Problem на Neotolis Engine.

Not a Trolley Problem это инкременталка про проблему вагонетки. Идея в том, чтобы взять моральную дилемму и довести ее до полного абсурда. Вместо того чтобы решать, кого спасти, вы таскаете людей на рельсы, получаете деньги, покупаете улучшения и постепенно автоматизируете весь процесс.

Сама игра появилась не с нуля. До этого у меня уже был 2D-прототип на PixiJS, который я сделал за 13 дней для Gamedev.js Jam. Основная идея, механики и понимание того, что я хочу получить, уже были.

Базовый loop заработал довольно быстро: толпа, трамвай, можно хватать людей, бросать их на рельсы и получать монетки.

Дальше итеративно улучшал игру. Добавлял механики, переделывал UI, пробовал разный визуал, менял баланс. Всё, что сейчас есть в игре, сделал ИИ. Я решал, что нужно сделать, смотрел на результат и давал правки.

На игре очень быстро стало видно, чего движку не хватает. Пока делаешь отдельные демо, многие вещи просто не встречаются. А тут они начали появляться сами: для теней понадобился depth compare sampler, для post-processing RGBA16F render targets, для оптимизации пришлось переделать работу с буферами.

До игры о части этих вещей я просто не думал. Реальный проект проверяет движок намного лучше отдельных технических демо.

За месяц игра уже стала похожа на демо. Я добавил прокачку, автоматизацию, разных людей и боссов, сохранение, обучение и две локализации. Собрал версии для браузера и Windows.

А примерно через месяц после начала 3D-версии уже показывал игру на Comic Con Tashkent и давал играть людям прямо на стенде.

Заключение

Сейчас Not a Trolley Problem больше демо, чем готовая игра. В игре уже работает основной цикл, есть контент и её можно дать людям поиграть, но работы впереди ещё много.

Для меня важно что на Neotolis Engine уже можно делать не только отдельные технические демо, а собирать настоящую игру и постепенно развивать движок вместе с ней.

Конечно, на готовом движке я бы сделал эту игру быстрее. Но я люблю делать тех.

Я рассчитываю, что время, которое сейчас уходит на Neotolis Engine, начнёт окупаться дальше. На второй игре уже не придётся снова писать половину вещей, на третьей ещё меньше, и постепенно у меня будет свой набор инструментов, который подходит именно под мои игры.

Not a Trolley Problem я планирую довести до полноценной игры. А дальше хочу делать на Neotolis Engine следующие проекты и развивать движок уже вместе с ними.

Ссылки

Комментарии (2)


  1. Void-Cowboy
    19.08.2026 14:20

    как раз именно такие моменты поддерживают во мне веру в то что бум LLM это хорошо

    успехов вам с вашим продуктом и не забрасывайте - легковесные хорошие вещи сейчас все еще редки


  1. KeyJoo
    19.08.2026 14:20

    +1 в карму. Когда мелкий пацан учил Хенкока(в фильме) подбадривать людей - его коронная фраза была: “Молодцом!”(в реальности тоже актуально)