Разработка современных UI при помощи DHTML

Если ты это читаешь, то ты как и я, был рад chromium’у в Garry’s Mod, несомненно это дало нам огромное пространство для UI/UX, от обычных чат-боксов до всего интерфейса перед глазами игроков.
Начнем!

Рекомендации и личные советы

Один файл

При создании UI с использованием HTML конечно будет плюсом если не придется дополнительно арендовывать хостинг под худ или иной интерфейс, поэтому всегда стараемся на выходе получить один файл .html в котором будут стили, скрипты и верстка. Если же вы не скупы на хост или умеете использовать Vercel / GitHub Pages, то для вас все двери открыты) (плюсом получите почти realtime обновление интерфейса без манипуляций с Lua файлами сервера, но учтите что Vercel или другие бесплатные хостинги могут быть не доступны в стране игрока)

Один инстанс на весь интерфейс

Старайтесь не плодить множества DHTML под разные элементы интерфейса, в идеале будет если вы поместите абсолютно весь интерфейс в один DHTML инстанс. Запомните каждый DHTML в вашем проекте это + 1 subprocess chromium’а, не у всех игроков дома стоит квантовый пк)

Локальная разработка

Если разработка вашего UI подразумевает использовать hotload обновление по типу dev-Vite, то в параметрах запуска Garry’s Mod обязательно укажите: -allowlocalhttp, данный параметр разрешает обращение к локальным ip вашей сети (localhost, 192.168…), не забудьте потом убрать данный параметр, а то плохие люди смогут снести ваш роутер или того хуже…

Сложная логика

Надо понимать что пк игроков не резиновый, поэтому стараемся не делать слишком сложную логику, особенно если у вас Virtual DOM фреймворк. Сама игра лагать не сильно должна, но fps внутри фрейма(dhtml) может значительно упасть.

Первый HUD

HTML Часть

В данном туторе я не буду использовать фреймворки, ограничимся ванильным js,html,css.

Для начала создадим hud.html файл в любом месте пк (не обязательно именно такое название, главное что с расширением .html), к примеру на рабочем столе, открою его в любом текстовом редакторе (кроме Word и ему подобных ) и напишу базовый код (помним, мы делаем интерфейс, а не сайт, поэтому на семантику и лишние теги нам плевать :slight_smile: )

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            font-family: "Roboto", sans-serif;
        }

        .container {
            position: fixed;
            bottom: 0.5rem;
            left: 0.5rem;
            display: flex;
            flex-direction: row;
            align-items: end;
            gap: 0.8rem;
            background: rgba(31, 29, 14, 0.5);
            border-radius: 0.8rem;
            padding: 1.1rem 1.2rem;
            color: rgb(255, 213, 0)
        }

        .container span:first-child {
            font-size: 0.7rem;
        }

        .container span:last-child {
            line-height: 2rem;
            position: relative;
            font-weight: bold;
            font-size: 2.6rem;
            color: rgb(255, 213, 0);
            text-shadow: 0 0 5px rgba(255, 213, 0, 0.187), 0 0 10px rgb(255, 213, 0, 0.4), 0 0 20px rgb(255, 213, 0, 0.1);
        }
    </style>
</head>

<body>

    <div class="container">
        <span>HEALTH</span>
        <span data-var="health">100</span>
    </div>

    <script>
        // Пример функции обновления значения переменной
        function updateVar(varName, value) {
            // Находим ВСЕ элементы с атрибутом data-var равным varName
            const elements = document.querySelectorAll(`[data-var="${varName}"]`);

            // Проходим по всем найденным элементам и обновляем их содержимое
            elements.forEach(element => {
                // Обновляем текстовое содержимое
                element.textContent = value;
            });
        }
    </script>
</body>
</html>

Что тут происходит?
Пояснять за HTML не буду, вы сможете загуглить, но суть в том что мы МОЖЕМ из lua вызывать глобально доступные JavaScript функции, да и в целом выполнять любой JavaScript код. Мы конечно могли просто в lua вызвать

const elements = document.querySelectorAll(`[data-var="health"]`);
elements.forEach(element => {
	element.textContent = value;
});

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

Тут же мы из луа будем вызывать функцию updateVar(varName, value), где в varName указываем что собираемся менять и в value на что меняем. Далее скрипт ищет все элементы с атрибутом data-var="some-var" и меняет их текстовый контент на значение из value. Все просто!

Кстати мы можем запустить данный html в любом браузере и проверить что все работает!


Открываем консоль разработчика и вводим туда updateVar('health', 44).

Если значение поменялось, то все работает!

Lua часть

Теперь в любой клиентской части добавим DHTML куда вставим наш HTML код.

-- Client side
local PANEL = {}
PANEL.health = 0

function PANEL:Init()
    -- Размер панели на весь экран
    self:SetSize(ScrW(), ScrH())
    
    -- Сетаем на нулевую позицию
    self:SetPos(0, 0)
    
    -- На всякий двигаем назад
    self:SetPopupStayAtBack( true )

    -- Загружаем HTML
    self:SetHTML([[<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            font-family: "Roboto", sans-serif;
        }

        .container {
            position: fixed;
            bottom: 0.5rem;
            left: 0.5rem;
            display: flex;
            flex-direction: row;
            align-items: end;
            gap: 0.8rem;
            background: rgba(31, 29, 14, 0.5);
            border-radius: 0.8rem;
            padding: 1.1rem 1.2rem;
            color: rgb(255, 213, 0)
        }

        .container span:first-child {
            font-size: 0.7rem;
        }

        .container span:last-child {
            line-height: 2rem;
            position: relative;
            font-weight: bold;
            font-size: 2.6rem;
            color: rgb(255, 213, 0);
            text-shadow: 0 0 5px rgba(255, 213, 0, 0.187), 0 0 10px rgb(255, 213, 0, 0.4), 0 0 20px rgb(255, 213, 0, 0.1);
        }
    </style>
</head>

<body>

    <div class="container">
        <span>HEALTH</span>
        <span data-var="health">100</span>
    </div>

    <script>
        // Пример функции обновления значения переменной
        function updateVar(varName, value) {
            // Находим ВСЕ элементы с атрибутом data-var равным varName
            const elements = document.querySelectorAll(`[data-var="${varName}"]`);

            // Проходим по всем найденным элементам и обновляем их содержимое
            elements.forEach(element => {
                // Обновляем текстовое содержимое
                element.textContent = value;
            });
        }
    </script>
</body>
</html>
    ]])
end

-- Нет смысла каждый тик слать текущее здоровье игрока,
-- стоит слать только когда значение изменилось.
function PANEL:Think()
    local ply = LocalPlayer()
    if not IsValid(ply) then return end
    local currentHealth = ply:Health()
    if (self.health == currentHealth) then return end
    
    -- Обновляем здоровье
    self.health = math.max(0, currentHealth)
    self:RunJavascript(string.format("updateVar('health', %d)", self.health))
end

vgui.Register("CustomHUD", PANEL, "DHTML")

-- Создаем HUD
local hudPanel

hook.Add("HUDPaint", "CustomHUD_Paint", function()
    if not IsValid(hudPanel) then
        hudPanel = vgui.Create("CustomHUD")
    end
end)

-- Скрываем стандартный HUD
local hideHUD = {
    ["CHudHealth"] = true,
}

hook.Add("HUDShouldDraw", "CustomHUD_HideDefault", function(name)
    if hideHUD[name] then
        return false
    end
end)

Заходим в игру и проверяем.


Собственно все работает успешно)

Итоги

Теперь у вас есть базовый, но полностью рабочий HUD для Garry’s Mod!
На этой базе уже можно делать интересные UI для вашего проекта

Фреймворки

Перед созданием проекта, поговорим про фреймворки на которых также возможно разрабатывать интерфейс для Garry’s Mod.

Virtual DOM фреймворки (Vue, React, Preact)

Несомненно можно использовать все фреймворки, особых ограничений нет, но самым важным является производительность. Virtual DOM фреймворки добавляют дополнительный слой абстракции между вашим кодом и реальным DOM, что может негативно сказаться на FPS внутри DHTML панели, особенно при частых обновлениях данных типа здоровья, патронов или позиции игрока.

С другой стороны, фреймворки предоставляют мощные инструменты для разработки сложных интерфейсов. Реактивность из коробки означает что вам не нужно вручную обновлять каждый элемент UI, компонентная архитектура позволяет переиспользовать код, а богатая экосистема дает доступ к тысячам готовых решений. TypeScript поддержка помогает избежать многих ошибок еще на этапе разработки.

Однако стоит помнить что любой фреймворк увеличивает итоговый размер вашего бандла. React весит около 40kb в минифицированном виде, Vue примерно 30kb, что может быть критично если вы встраиваете HTML через SetHTML. Также для работы с фреймворками потребуется настроить сборщик типа Vite или Webpack, что усложняет процесс разработки.

Рекомендации по выбору

Если вы делаете простой HUD с несколькими элементами, лучше остановиться на ванильном JavaScript. Он не требует сборки, работает быстрее всего и занимает минимум места. Для средних по сложности проектов хорошим выбором будет Preact - это облегченная альтернатива React весом всего 3kb, которая сохраняет почти всю функциональность оригинала.

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

Альтернативы

Стоит также рассмотреть современные компиляторы типа Svelte. В отличие от традиционных фреймворков, Svelte компилирует ваш код в оптимальный vanilla JavaScript на этапе сборки, что означает нулевой runtime overhead. Вы получаете все преимущества реактивности и компонентов, но итоговый код работает так же быстро как написанный вручную.

Для тех кто хочет минимализм но с удобством фреймворка, можно посмотреть на Alpine.js или Petite-Vue - это крошечные библиотеки весом 10-15kb, которые добавляют реактивность прямо в HTML через специальные атрибуты, не требуя сложной настройки.


upd: Хотел с начала просто кинуть ссылку на другое сообщество, но посчитал это наглым, если вы не против буду благодарен за посещение этого форума :heart::

1 лайк