Изучаем Dear ImGUI в Defold. Часть 1. Создание окна и таблицы с полями

Предисловие


В этом пособие я использовал версию:
https://github.com/britzl/extension-imgui/archive/refs/tags/2.6.1.zip


В этом уроке мы познакомимся с графическим интерфейсом Dear ImGUI в Defold.
Он идеально подходит для быстрого создания отладочного интерфейса или создания внутриигровых инструментов и настроек.

Я настоятельно рекомендую его использовать в своей работе, т.к освоив его, вы сэкономите время на разработку инструментов отладки/тестирования вашей игры.
Кстати, сейчас я его использую в нашей игре с ребятами: https://defolder.com/t/arnulf-protiv-vseh/93


Что советую и вам, по крайней мере, других альтернатив я не знаю.

Это урок прежде всего нацелен на тех, кто знаком с основами Defold: умеет создавать игровые объекты, компоненты, понимает как работает скрипт и из чего он состоит, что такое коллекция и т.д, и т.п.
Если не знакомы с Defold, рекомендую ознакомиться с материалы по Defold на официальном сайте: Defold manuals and other learning resources

Несмотря на все вышеперечисленное, вы можете повторить все действия и увидеть результат в этом уроке.


Подключаем библиотеку и добавляем input_bindings


Добавим Dear ImGui в качестве зависимости в ваш проект:

После того, как вы добавили imgui в ваш проект и подтянули зависимости, в вашем проекте должна появиться папка imgui:
image

Файл imgui.input_binding скопируем (ctrl + C) перенесём в нашу папку input (ctrl + V):


В game.project, в Runtime input, в Game Binding изменим файл с привязками ввода:

Вы можете удалить game.input_binding, а также сменить название для вашего input_binding.


Создаём игровой объект, добавляем скрипты


Создадим main.script:
image

В main.collection создаём игровой объект и добавляем компоненты:

Скрипт imgui появился в результате добавления Dear ImGUI в наш проект.
Этот скрипт — это обработчик ввода, написанный за нас.

Перейдите в скрипт main и вставьте этот код:

-- Пример данных: таблица с заголовками и строками

local headers = { "ID", "Name", "Value" }

local rows = {
    { 1, "Alpha", 10 },
    { 2, "Bravo", 20 },
    { 3, "Charlie", 30 },
}

-- Функция инициализации 
function init(self)
    msg.post(".", "acquire_input_focus")
    imgui.set_ini_filename()      
end

-- Отрисовка панели
function update(self, dt)
    -- Начало окна с авторазмером
    imgui.begin_window("Simple Table", nil, imgui.WINDOWFLAGS_ALWAYS_AUTO_RESIZE)
    -- Начало таблицы: 3 столбца
    if imgui.begin_table("table1", #headers) then
        -- Установка заголовков
        for _, h in ipairs(headers) do
            imgui.table_setup_column(h)
        end
        imgui.table_headers_row()
        -- Заполнение строк
        for _, row in ipairs(rows) do
            imgui.table_next_row()
            for i, cell in ipairs(row) do
                imgui.table_next_column()
                imgui.text(tostring(cell))
            end
        end
        imgui.end_table()
    end
    imgui.end_window()
end

Сохраните проект (ctrl + S), соберите и запустите его (ctrl + B):


Справочник


В ImGui каждый контейнер (окно, таблица, вкладка и т. д.) открывается явным вызовом begin_* и должен «закрываться» соответствующим end_*. Это нужно, чтобы:

  1. Синхронизировать стек состояний
    ImGui хранит внутренний стек активных контейнеров. При begin_window или begin_table в этот стек добавляется новая запись с параметрами (позиция, размер, стиль, колонки и т. д.). Если не вызвать end_window или end_table, контейнер останется «незакрытым», что приводит к рассинхронизации стека и, как следствие, к артефактам рендеринга или даже крашу.
  2. Завершить сборку содержимого
    end_table() говорит ImGui: «все ячейки добавлены — можешь вычислить итоговые размеры, нарисовать бордюры, обработать возможный скроллинг и т. д.».
    end_window() сигнализирует о том, что в это окно больше ничего не рисуется, и его можно «слить» с остальным интерфейсом.
  3. Оптимизация
    Только после полного закрытия контейнера ImGui может оптимизировать отрисовку (отсечь невидимые элементы, свернуть колонки, рассчитать прокрутку), исходя из полного набора виджетов внутри.

Поэтому всегда:

imgui.begin_window("Title", ...)
    -- содержимое окна
imgui.end_window()

-- и для таблицы:
if imgui.begin_table("id", column_count) then
    -- настройка и наполнение таблицы
    imgui.end_table()
end


imgui.set_ini_filename(filename)

imgui.set_ini_filename(filename)

Функция imgui.set_ini_filename(filename) задаёт имя файла, в который ImGui будет сохранять и из которого загружать свою конфигурацию окна (позиции, размеры, состояния свёрнутости и т. д.) при старте/выходе.

Если вызвать imgui.set_ini_filename() без аргумента или с nil, то ImGui отключит автоматическую загрузку и сохранение настроек в .ini файл. То есть:

  • Без вызова — ImGui по умолчанию создаст и будет использовать файл imgui.ini рядом с бинарником.
  • С imgui.set_ini_filename("custom.ini") — будет использовать custom.ini.
  • С imgui.set_ini_filename() (или nil) — отключает работу с .ini вовсе, окно всегда рисуется в месте и размере, заданном в коде.

imgui.begin_table()

imgui.begin_table("table1", #headers)

Выполняет следующие действия:

  1. Создаёт новую таблицу с уникальным идентификатором "table1".
  2. Устанавливает число колонок равным значению #headers (длине массива headers).
  3. Инициализирует внутренние структуры ImGui для управления разметкой ячеек, заголовков и прокрутки (если позже указаны соответствующие флаги).
  4. Возвращает true, если таблица готова к заполнению (например, при наличии достаточного места для рендеринга). Если возвращается false, значит таблица не может быть отрисована на текущем кадре, и все вызовы заполнения (table_setup_column, table_next_row и т. д.) следует пропустить до imgui.end_table().

После успешного begin_table вы настраиваете колонки через imgui.table_setup_column(), рисуете заголовки функций imgui.table_headers_row(), заполняете строки и ячейки с помощью imgui.table_next_row() и imgui.table_next_column(), а затем закрываете таблицу вызовом imgui.end_table().


imgui.table_setup_column(h)

imgui.table_setup_column(h)

Выполняет настройку текущей колонки таблицы:

  • Аргумент h (строка) задаёт текст заголовка для этой колонки, который позже отрисуется при вызове imgui.table_headers_row().
  • По умолчанию каждая колонка получает автоматическую ширину, выравнивание и флаги.
  • Функция подготавливает внутренние параметры (ширину, приоритет изменения размера, флаги выравнивания) для последующего рендеринга ячеек в этой колонке.

Если нужен более тонкий контроль, можно передать дополнительные аргументы:

imgui.table_setup_column(column_id, flags, init_width_or_weight, user_id)

  • flags — флаги поведения колонки (например, фиксированная ширина, возможность сортировки).
  • init_width_or_weight — начальная ширина или вес, влияющий на перераспределение свободного пространства.
  • user_id — целочисленный идентификатор для сортировки.

Обычно, imgui.table_setup_column(h) достаточно, чтобы задать шапку и автоматически распределить колонки по контенту.


imgui.table_headers_row()

imgui.table_headers_row()

Служит для отрисовки строки заголовков таблицы. После того как вы настроили колонки вызовами imgui.table_setup_column(), этот вызов:

  1. Создаёт новую строку в таблице.
  2. В каждой колонке выводит текст заголовка, указанный в table_setup_column.
  3. Рисует визуальное разделение между заголовками (если используется стиль с линиями).

Без вызова table_headers_row() таблица не покажет шапку с названиями колонок — вы получите только пустую область для данных.


imgui.table_next_column()

imgui.table_next_column()

Выполняет переход к следующей ячейке в текущей строке таблицы.

Подробности:

  • При вызове внутри тела таблицы этот вызов устанавливает «курсор» рендеринга в следующую колонку.
  • Если вы вызвали imgui.table_next_row() перед этим, то сначала курсор сядет в первую колонку новой строки, а последующие table_next_column() последовательно переходят по всем столбцам.
  • В каждой ячейке вы затем размещаете виджет (например, imgui.text, imgui.button и т.д.).
  • Когда количество вызовов table_next_column() достигает числа колонок, дальнейшие вызовы будут игнорироваться до начала новой строки.

imgui.text(str)

imgui.text(str)

Функция imgui.text(str) выводит в текущей позиции окна простой текстовый виджет с содержимым str.

  • Берёт строку (или преобразованную через tostring) и рисует её в интерфейсе.
  • Не реагирует на ввод — только отображает статический текст.
  • После отрисовки курсор смещается вниз на высоту строки, чтобы следующий вызов рисовал ниже.

Функция imgui.set_ini_filename(filename) задаёт имя файла, в который ImGui будет сохранять и из которого загружать свою конфигурацию окна (позиции, размеры, состояния свёрнутости и т. д.) при старте/выходе.

Если вызвать imgui.set_ini_filename() без аргумента или с nil, то ImGui отключит автоматическую загрузку и сохранение настроек в .ini файл. То есть:

  • Без вызова — ImGui по умолчанию создаст и будет использовать файл imgui.ini рядом с бинарником.
  • С imgui.set_ini_filename("custom.ini") — будет использовать custom.ini.
  • С imgui.set_ini_filename() (или nil) — отключает работу с .ini вовсе, окно всегда рисуется в месте и размере, заданном в коде.

Cоздаёт окно с заголовком “Simple Table”, которое подстраивается под содержимое.

imgui.begin_window("Simple Table", nil, imgui.WINDOWFLAGS_ALWAYS_AUTO_RESIZE) 

WINDOWFLAGS_ALWAYS_AUTO_RESIZE — это флаг при создании окна ImGui, который заставляет окно автоматически подстраивать свой размер под содержимое при каждом кадре.

Подробно:

  • При наличии этого флага ImGui не рисует рамку, позволяющую пользователю вручную тянуть границы окна — вместо этого ширина и высота окна вычисляются исходя из максимальных размеров виджетов внутри.
  • Если вы добавляете или удаляете элементы интерфейса (текст, кнопки, таблицы), окно моментально «обхватывает» их по размеру без лишнего пустого пространства.
  • Полезно для небольших вспомогательных или отладочных панелей, где критична компактность и отсутствуют статические размеры.
  • Не подходит для окон со скроллингом или когда нужно сохранить постоянную область отображения при динамическом контенте.

Пример использования:

imgui.begin_window("My Window", nil, imgui.WINDOWFLAGS_ALWAYS_AUTO_RESIZE)   
	-- содержимое... 
imgui.end_window()

В этом случае окно «My Window» всегда точно соответствует размеру своих внутренних виджетов.

Каждый элемент означает следующее:

  1. imgui.begin_window и imgui.end_window
    Эти два вызова всегда идут парой. Всё, что рисуется между ними, попадает внутрь одного окна.
  2. Первый аргумент "My Window"
    Заголовок окна. В нём будет отображаться текст в заголовочной полосе окна.
  3. Второй аргумент nil
    Опциональный аргумент — указатель на булевую переменную (флажок) для контроля видимости окна.
    • Если передать ссылку на true/false, ImGui сам сможет закрывать окно (по крестику) и менять эту переменную.
    • Если nil, окно всегда открыто, а кнопка закрытия не рисуется.
  4. Третий аргумент imgui.WINDOWFLAGS_ALWAYS_AUTO_RESIZE
    Флаг, заставляющий окно автоматически подстраивать свои ширину и высоту под содержимое на каждом кадре.

Следующая тема: Изучаем Dear ImGUI в Defold. Часть 2. Радиокнопки

Всем спасибо за внимание!