Назад к блогу

Defold переходит в полное 3D: технический разбор и туториал по архитектуре

Опубликовано 12 мая 2026 г.
Defold переходит в полное 3D: технический разбор и туториал по архитектуре

Коротко о главном

Статья подробно разбирает трансформацию Defold в полноценный 3D game engine и описывает низкоуровневую работу его rendering pipeline на Lua. Вы узнаете, как реализовать First-Person камеру, оптимизировать GLSL шейдеры для WebGL и настраивать эффективный Netcode для Multiplayer. Особое внимание уделено интеграции Backend-инфраструктуры horizOn для масштабирования игровых сервисов без необходимости ручного управления серверами.

Каждый инди-разработчик знает момент, когда игровой движок его предает. Вы начинаете с легковесного 2D концепта, scope creep добавляет 3D-элементы, и внезапно размер билда взлетает, время загрузки увеличивается, а web-билды вылетают с ошибкой out of memory. Такие Massive engines, как Unity и Unreal, феноменальны для AAA fidelity, но для соло-разработчиков, нацеленных на бесшовную кроссплатформенную дистрибуцию — особенно WebGL и mobile — они часто кажутся танком для поездки в магазин.

Встречайте Defold. Исторически известный как ультрабыстрый 2D движок без лишнего мусора (zero-bloat), используемый студиями для охвата всех платформ от HTML5 до Nintendo Switch с единым codebase, Defold официально повзрослел. Теперь это полноценный 3D game engine. Хотя технически он всегда рендерил в 3D-контексте (проецируя плоские грани через orthographic camera), последние обновления добавили специализированный 3D инструментарий, полную поддержку glTF mesh и оптимизированный workflow для настоящей 3D-разработки.

Это серьезный сдвиг для экосистемы. Если вы ищете движок, который компилируется за секунды, выдает бинарные файлы размером в несколько мегабайт и при этом дает возможность рендерить динамические 3D-миры, Defold теперь является топовым претендентом. В этом defold 3d game engine tutorial и архитектурном разборе мы глубоко погрузимся в работу Defold rendering pipeline, напишем кастомную 3D-камеру с нуля и разберем Backend-нюансы реализации сетевой части для легковесной 3D-игры.

Under the Hood: Реальность 3D-пайплайна в Defold

Чтобы мастерски использовать Defold в 3D, нужно понимать его философию рендеринга. Defold не дает преднастроенный PBR (Physically Based Rendering) pipeline «из коробки», как это делает Unreal Engine. Вместо этого он предоставляет высокооптимизированный, data-driven render script на Lua.

Все, что рисуется на экране в Defold, обрабатывается через render_script. По умолчанию этот скрипт настроен для 2D. Он создает orthographic projection matrix, сортирует спрайты по Z-value (depth) и отрисовывает их от дальних к ближним. Чтобы разблокировать 3D-возможности Defold, мы должны переписать этот скрипт, используя perspective projection matrix, включить hardware depth testing и определить кастомные render predicates для наших 3D-моделей.

Такой low-level access — это палка о двух концах. С одной стороны, придется немного поработать с матричной математикой. С другой — у вас есть абсолютный контроль над draw calls, что позволяет оптимизировать рендеринг для самого слабого железа так, как это невозможно в монолитных движках.

Архитектура вашего кастомного 3D Render Script

Чтобы рендерить честные 3D-модели без ошибок перекрытия, нам нужно включить depth buffer (Z-buffer). Вот базовый 3D render script, который заменяет стандартный пайплайн Defold.

-- main/3d_pipeline.render_script
function init(self)
    -- Определяем цвет очистки фона (RGBA)
    self.clear_color = vmath.vector4(0.1, 0.1, 0.12, 1.0)
    self.clear_buffers = {
        [render.BUFFER_COLOR_BIT] = self.clear_color,
        [render.BUFFER_DEPTH_BIT] = 1.0,
        [render.BUFFER_STENCIL_BIT] = 0
    }

    -- Создаем предикаты рендеринга. 'model' — стандартный тег для 3D мешей
    self.predicates = {
        model = render.predicate({"model"}),
        gui = render.predicate({"gui"}),
        text = render.predicate({"text"})
    }
end

function update(self)
    -- 1. Настраиваем состояние рендеринга для кадра
    render.set_depth_mask(true)
    render.set_stencil_mask(0xff)
    render.clear(self.clear_buffers)

    -- 2. Конфигурируем проекцию 3D-камеры
    local window_width = render.get_window_width()
    local window_height = render.get_window_height()
    if window_width == 0 or window_height == 0 then return end
    
    local aspect_ratio = window_width / window_height
    local fov = math.rad(60) -- 60 градусов Field of View
    local near_z = 0.1
    local far_z = 1000.0
    
    local proj_matrix = vmath.matrix4_perspective(fov, aspect_ratio, near_z, far_z)
    render.set_projection(proj_matrix)

    -- 3. Рисуем 3D-модели с включенным Depth Testing
    render.set_depth_test(render.COMPARE_LEQUAL)
    render.set_cull_face(render.FACE_BACK)
    render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
    
    -- View matrix передается через сообщения из нашего скрипта камеры
    if self.view_matrix then
        render.set_view(self.view_matrix)
        render.draw(self.predicates.model)
    end

    -- 4. Рисуем GUI поверх 3D-сцены (Orthographic)
    render.set_depth_mask(false)
    render.set_depth_test(render.COMPARE_ALWAYS)
    local gui_proj = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
    render.set_projection(gui_proj)
    render.set_view(vmath.matrix4())
    
    render.draw(self.predicates.gui)
    render.draw(self.predicates.text)
end

function on_message(self, message_id, message)
    if message_id == hash("set_view_matrix") then
        self.view_matrix = message.matrix
    end
end

Обратите внимание, насколько явно мы управляем состоянием. Мы очищаем depth buffer, рассчитываем perspective matrix на основе текущих размеров окна, принудительно используем back-face culling для экономии циклов растеризации и, наконец, переключаем projection matrix обратно в orthographic перед рендерингом GUI. Это дает вам надежную архитектуру с разделенным пайплайном.

Создание First-Person 3D контроллера камеры

Сам по себе render script мало что даст без камеры, движущейся в пространстве. Defold активно использует архитектуру Message Passing. В отличие от объектно-ориентированных движков, где камера может напрямую вызывать transform.Translate(), в Defold наш скрипт камеры будет рассчитывать view matrix и отправлять её в render script, который мы только что написали.

Давайте создадим стандартный First-Person контроллер, который обрабатывает mouse-look (pitch и yaw) и перемещение на клавиатуре (WASD).

-- scripts/camera_controller.script
go.property("mouse_sensitivity", 0.2)
go.property("move_speed", 10.0)

function init(self)
    msg.post(".", "acquire_input_focus")
    self.pitch = 0
    self.yaw = 0
    
    -- Скрываем и захватываем курсор мыши
    window.set_mouse_lock(true)
    
    self.forward = vmath.vector3(0, 0, -1)
    self.right = vmath.vector3(1, 0, 0)
    self.up = vmath.vector3(0, 1, 0)
    self.velocity = vmath.vector3(0)
end

function update(self, dt)
    local pos = go.get_position()
    
    -- Применяем скорость перемещения
    if vmath.length_sqr(self.velocity) > 0 then
        local move_dir = vmath.normalize(self.velocity)
        pos = pos + move_dir * self.move_speed * dt
        go.set_position(pos)
    end

    -- Рассчитываем view matrix
    local rotation = go.get_rotation()
    self.forward = vmath.rotate(rotation, vmath.vector3(0, 0, -1))
    local target = pos + self.forward
    
    local view_matrix = vmath.matrix4_look_at(pos, target, self.up)
    
    -- Отправляем рассчитанную view matrix в render pipeline
    msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
    
    -- Сбрасываем скорость для следующего кадра
    self.velocity = vmath.vector3(0)
end

function on_input(self, action_id, action)
    if action_id == hash("mouse_moved") then
        self.yaw = self.yaw - action.dx * self.mouse_sensitivity
        self.pitch = self.pitch + action.dy * self.mouse_sensitivity
        
        -- Ограничиваем pitch, чтобы камера не переворачивалась
        self.pitch = math.max(-89, math.min(89, self.pitch))
        
        -- Конвертируем углы Эйлера в Quaternion
        local rot_y = vmath.quat_rotation_y(math.rad(self.yaw))
        local rot_x = vmath.quat_rotation_x(math.rad(self.pitch))
        go.set_rotation(rot_y * rot_x)
        
    elseif action_id == hash("move_forward") then
        self.velocity = self.velocity + self.forward
    elseif action_id == hash("move_backward") then
        self.velocity = self.velocity - self.forward
    elseif action_id == hash("move_left") then
        self.right = vmath.cross(self.forward, self.up)
        self.velocity = self.velocity - self.right
    elseif action_id == hash("move_right") then
        self.right = vmath.cross(self.forward, self.up)
        self.velocity = self.velocity + self.right
    end
end

Этот скрипт захватывает дельту движения мыши для настройки pitch и yaw, преобразуя эти углы Эйлера в надежную ротацию через Quaternion. Затем он извлекает векторы forward и right напрямую из ротации, чтобы нажатие «W» всегда перемещало вас в направлении, куда вы смотрите в данный момент.

Интеграция 3D-ассетов и кастомные шейдеры

С последними обновлениями Defold импорт 3D-ассетов стал тривиальным. Движок нативно поддерживает форматы .gltf и .glb, которые стали индустриальным стандартом для веба и разработки легковесных игр.

Однако для рендеринга меша нужен материал, а материалам нужны шейдеры. По умолчанию Defold включает базовые материалы, но написание собственных GLSL shaders дает визуальную уникальность, необходимую для того, чтобы выделиться. Напишем быстрый unlit textured shader, который идеально оптимизирован для mobile или HTML5.

Vertex Shader (model.vp)

// model.vp
uniform highp mat4 view_proj;
uniform highp mat4 world;

attribute highp vec4 position;
attribute mediump vec2 texcoord0;

varying mediump vec2 var_texcoord0;

void main()
{
    // Рассчитываем итоговую позицию вершины в screen-space
    vec4 p = view_proj * world * vec4(position.xyz, 1.0);
    var_texcoord0 = texcoord0;
    gl_Position = p;
}

Fragment Shader (model.fp)

// model.fp
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D texture_sampler;
uniform lowp vec4 tint;

void main()
{
    // Выбираем сэмпл текстуры и умножаем на цветовой tint
    lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
    gl_FragColor = tex_color * tint;
}

В файле материала Defold вы привязываете uniform view_proj к встроенной матрице view-projection движка, а texture_sampler — к diffuse texture вашего меша. Поскольку эти шейдеры не рассчитывают динамическое освещение или shadow maps, они работают невероятно быстро, позволяя легко поддерживать 60 FPS на слабом железе.

Работа с 3D Multiplayer и синхронизация состояния

При переходе от 2D игры на сетке к полноценному 3D окружению сложность вашей сетевой архитектуры растет экспоненциально. В 3D синхронизация состояния должна учитывать Z-axis depth, погрешности floating-point в физических движках и полные ротации через кватернионы. Если подойти к этому неправильно, вы получите серьезный visual stuttering — распространенную проблему, которую мы рассматривали в статье How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.

Поскольку Defold — легковесный движок, в нем нет тяжелой встроенной системы репликации вроде RPC в Unreal Engine. Вы сами отвечаете за эффективную упаковку данных состояния.

Использование REST APIs для синхронизации 3D-позиций мгновенно создаст bottleneck в вашем game loop. Вместо этого нужны постоянные двусторонние соединения. В то время как наше предыдущее руководство охватывало тему Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends, те же самые архитектурные принципы напрямую применимы к Lua-расширениям WebSockets в Defold.

Вот пример того, как следует сериализовать данные 3D-трансформа для минимизации payload через WebSockets:

-- scripts/network_sync.lua
local json = require "builtins.scripts.json"

function serialize_transform(go_id)
    local pos = go.get_position(go_id)
    local rot = go.get_rotation(go_id)
    
    -- Сжимаем payload до абсолютного минимума необходимых данных
    -- Округляем позицию до 2 знаков после запятой для экономии трафика
    local payload = {
        id = hash_to_hex(go_id),
        x = math.floor(pos.x * 100) / 100,
        y = math.floor(pos.y * 100) / 100,
        z = math.floor(pos.z * 100) / 100,
        -- Кватернионы требуют все 4 компонента для точной реконструкции
        qx = rot.x,
        qy = rot.y,
        qz = rot.z,
        qw = rot.w
    }
    
    return json.encode(payload)
end

Округляя числа с плавающей запятой и упаковывая только самые важные данные, вы предотвращаете переполнение network buffers при быстрых обновлениях перемещения. Управление этим потоком — ключ к отзывчивому кроссплатформенному геймплею.

Инфраструктурное бремя кроссплатформенного 3D

Написание высокооптимизированного 3D-клиента на Defold приносит огромное удовлетворение. Движок не мешает, компилируется за миллисекунды и позволяет сфокусироваться исключительно на логике.

Однако, как только вы решите сделать эту 3D-игру мультиплеерной, добавить облачные сохранения или реализовать кроссплатформенные лидерборды, ваш фокус мгновенно сместится с разработки игры на server orchestration. Внезапно вы обнаружите, что пишете Dockerfiles, настраиваете Kubernetes clusters, сражаетесь с инстансами Redis для хранения состояния сессий и пытаетесь защитить свои WebSocket-шлюзы от DDoS-атак.

Самостоятельная сборка такой системы требует настройки load balancers, шардирования баз данных и управления SSL-сертификатами — это легко займет 4-6 недель работы только ради того, чтобы запустить надежный прототип.

С horizOn эти Backend-сервисы уже преднастроены, что позволяет вам релизить игру, а не инфраструктуру. horizOn предоставляет нативные интеграции для аутентификации пользователей, синхронизации баз данных в real-time и server-authoritative логики, идеально закрывая пробелы легковесных движков вроде Defold, которые не поставляются с проприетарными Backend-экосистемами. Вы сохраняете скорость работы крошечного клиентского движка, передавая тяжелую работу по серверной архитектуре на аутсорс.

Best Practices для разработки на Defold 3D

Если вы планируете создать коммерчески успешный 3D-проект на Defold, строго придерживайтесь этих архитектурных правил:

  1. Держите геометрию максимально оптимизированной: Defold создан для скорости. Чтобы сохранить преимущество в легковесности, держите геометрию уровня в пределах 100,000 полигонов на всю сцену, особенно если вы целитесь в HTML5/WebGL. Используйте запеченные normal maps вместо высокополигональных мешей для детализации.
  2. Используйте Render Predicates для Frustum Culling: Defold не отсекает объекты за пределами видимости камеры в 3D-пространстве автоматически. Вам нужно написать собственную логику Frustum Culling на Lua, динамически отключая компоненты моделей у объектов вне зоны видимости для экономии времени растеризации.
  3. Консолидируйте Draw Calls через атласинг: Каждый уникальный материал и текстура требуют отдельного draw call, отправляемого на GPU. Объединяйте свои текстуры в большие texture atlases. Если 10 различных 3D-моделей используют один и тот же материал и атлас, Defold сможет батчить их гораздо эффективнее.
  4. Предварительно рассчитывайте сложную математику: Перемножение матриц и конвертация кватернионов — довольно дорогие операции в Lua. Кэшируйте свои векторы forward и right и пересчитывайте их только тогда, когда ротация игрока действительно меняется, вместо того чтобы выполнять тяжелую математику безусловно в каждом кадре.
  5. Разделяйте логику и частоту рендеринга: Ваша игровая логика (update) может работать на 60 FPS, но ваши кастомные шаги физики или Netcode могут тикать на 30 FPS. Интерполируйте визуальные 3D-позиции на основе скорости, а не просто привязывайте их к последнему состоянию, чтобы обеспечить плавный рендеринг на мониторах с разной частотой обновления.
  6. Управляйте Lua Garbage Collection: В динамичном 3D-окружении вы часто создаете и уничтожаете объекты векторов и матриц. Lua Garbage Collector может вызывать заметные скачки фреймрейта, если его не контролировать. Переиспользуйте инстансы vmath.vector3 и vmath.matrix4 везде, где это возможно, обновляя их внутренние значения напрямую вместо создания новых локальных переменных внутри цикла update. Заранее выделяйте memory pools для пуль и сущностей.
  7. Запекайте освещение внешними инструментами: Так как динамическое освещение в кастомных GLSL шейдерах быстро съест ваш бюджет производительности на мобильных устройствах, запекайте Global Illumination и Ambient Occlusion прямо в текстуры в Blender или Maya перед экспортом glTF моделей. Простой unlit shader с красиво запеченным светом всегда будет работать лучше сложного динамического шейдера в мобильных браузерах.

Заключение

Эволюция Defold в полноценный 3D game engine — это огромная победа для независимых разработчиков. Он успешно сохраняет молниеносную компиляцию и невероятно крошечный размер бинарных файлов, предлагая при этом математическую базу и инструментарий, необходимые для создания масштабных и захватывающих 3D-миров. Освоив кастомные render scripts, понимая матричные операции и эффективно сериализуя сетевые данные, вы сможете создавать кроссплатформенные тайтлы, которые технически конкурируют с гораздо более тяжелыми и громоздкими движками.

Когда вы будете готовы вывести свой оптимизированный 3D-клиент в онлайн и масштабировать Multiplayer Backend без головной боли от управления «голой» инфраструктурой, попробуйте horizOn бесплатно или изучите API docs, чтобы увидеть, как быстро вы можете интегрировать real-time сервисы в свой следующий проект на Defold.


Источник: Defold is Now a Full 3D Game Engine