Files
Docs/docs/ru/beamng/dev/modding/ui-apps.md
2026-03-26 22:22:48 +00:00

13 KiB
Raw Permalink Blame History

Создание UI-приложения

Для создания UI-приложения вам понадобятся некоторые знания фреймворка AngularJS, основную документацию можно найти здесь: AngularJS docs

Структура файла

Для работы UI-приложения необходимы четыре важных файла:

  • app.js | Содержит основной код, используемый документацией UI-приложения Javascript
  • app.html | Код, который отображает HTML-документы вашего приложения
  • app.json | Содержит информацию о UI-приложении
  • app.png | Файл изображения, отображаемый в селекторе приложений

Стиль UI-приложения

Мы рекомендуем использовать тег <style> для стилизации вашего приложения. Файл .css тоже подойдет, но вы не сможете видеть изменения в режиме реального времени.

Пример

Этот пример от DanielW. Отдельная ему благодарность

ui\modules\apps\ExampleApp\app.html

<div style="width: 100%; height: 100%;" class="bngApp">
    <link type="text/css" rel="stylesheet" href="/ui/modules/apps/ExampleApp/app.css" />

    <div id="exampleAppContainer">
        <span>Gear: <span>{{ gearName }}</span></span>

        <div layout="row" layout-align="center center">
            <md-input-container flex>
                <label>Input</label>
                <input ng-model="message" ng-keydown="sendMessage($event)">
            </md-input-container>

            <md-button md-no-ink class="md-warn" ng-disabled="!message" ng-click="sendMessage()">Send</md-button>
        </div>

        <span style="display: block">Messages:</span>

        <!-- Область прокрутки -->
        <ul bng-nav-scroll style="margin: 0; padding: 0; overflow-y: auto; width: 100%; height: 100%; background-color: #37373740;">

            <!-- Перебор сообщений и их отображение -->
            <li ng-repeat="message in messages track by $index" style="display: flex; align-items: center; height: 35px;">
                <span style="padding: 0 0.2em; width: 100%;">{{ message }}</span>
        
                <!-- Кнопка для удаления сообщения, вызывает функцию `deleteMessage` в `app.js` -->
                <md-button md-no-ink class="md-icon-button md-warn" ng-click="deleteMessage($index)">
                    <md-icon class="material-icons">delete</md-icon>
                </md-button>
            </li>
        </ul>
    </div>
</div>

Здесь вы можете увидеть тег <span>, отображающий передачу вашего транспортного средства, входные данные, используемые для отправки сообщения функции sendMessage() в Javascript, и повторяющийся тег <li> использующий ng-repeat для переменной messages, расположенной в Javascript.

ui\modules\apps\ExampleApp\app.js

angular.module('beamng.apps')
.directive('exampleApp', [function() {
    return {
        templateUrl: '/ui/modules/apps/ExampleApp/app.html',
        replace: true,
        restrict: 'EA',
        scope: true,

        controller: ['$scope', function($scope) {
            $scope.gearName = '0'
            $scope.message  = ''
            $scope.messages = []

            // Настраиваем потоки, которые нам нужны. Пока что нам нужна только информация о двигателе.
            // Можно добавить больше, нужно будет просто поискать доступные потоки.
            let steamList = ['engineInfo']
            StreamsManager.add(steamList)

            $scope.$on('destroy', function() {
                StreamsManager.remove(steamList)
            })

            // Нужно ли вообще писать этот комментарий, объясняя, что делает эта функция?
            // Ну, я писал комментарии для многих других вещей, даже когда это было не нужно. Пусть уж этот останется...
            $scope.$on('streamsUpdate', function(event, streams) {
                if (!streams.engineInfo) // Ранний выход... Наверное, ты и без этого бесполезного комментария понял
                    return;

                // `lua/vehicle/controller/vehicleController.lua:538` (или можно использовать console.log)
                let gear = streams.engineInfo[5]

                // Обновляем название передачи в HTML при необходимости
                if ($scope.gearName !== gear)
                    $scope.gearName = gear
            })

            $scope.sendMessage = function(event) {
                if (event && event.key !== 'Enter')
                    return

                if ($scope.message == '')
                    return

                // Передаём сообщение в Lua-расширение, чтобы оно его изменило
                bngApi.engineLua('extensions.exampleMod.modifyMessage("' + $scope.message + '")')
                $scope.message = ''
            }

            $scope.deleteMessage = function(idx) {
                $scope.messages.splice(idx, 1)
            }

            // Функция `modifyMessage` вызовет этот хук с изменёнными данными
            $scope.$on('MessageReady', function(_, modifiedMessage) {
                $scope.messages.push(modifiedMessage)
            });
        }]
    }
}])

Обратите внимание на использование $scope{/b0}. Это очень важно, поскольку вам нужно будет определить переменные и функции в {b1}$scope, чтобы иметь возможность доступа к ним из Html внутри любого тега ng-*. Таким образом, в этом примере после выполнения функции sendMessage() из Html она отправит его в файл lua, расположенный в каталоге расширений мода, и выполнит функцию modifyMessage() внутри этого файла lua.

Пример того, как может выглядеть сторона lua:

local function modifyMessage(message)
    message = message .. " [Modified!]"
    guihooks.trigger('MessageReady', message)
end

^ Это упрощенная версия lua, просто показывающая функцию.

Основное внимание здесь уделяется использованию guihooks.trigger, который запускает событие AngularJS, определенное с помощью $scope.$on(). Как вы можете видеть в самом низу файла Javascript, событие называется MessageReady и будет выполнено функцией guihooks.trigger с полезной нагрузкой сообщения, а затем будет помещено в переменную $scope.messages для отображения тегом li с помощью ng-repeat в файле Html.

Полный файл lua находится ниже

lua\ge\extensions\exampleMod.lua

local M = {}

--[[
	Это точка входа нашего расширения — именно её загружает игра из файла `modScript.lua`.
	В файле modScript можно загружать больше расширений и помещать их в ту же директорию, что и этот файл.

	В этом файле мы будем взаимодействовать со следующими компонентами:
	1. Наше расширение для автомобиля. Оно сообщает этому расширению, когда нужно отправить данные, и мы их отправляем. См. `vehicle/extensions/auto/exampleVehicleExtension.lua`
	2. Ввод. См. `core/input/actions/myActions.json`. Когда привязанная клавиша нажата, вызывается `onActionKeyDown` (функция, экспортируемая ниже)
]]

-- Хуки игровых функций
--------------------------------------------
local function onExtensionLoaded()
	log('D', "onExtensionLoaded", "Called")
end

local function onExtensionUnloaded()
	log('D', "onExtensionUnloaded", "Called")
end

-- Пользовательские функции
--------------------------------------------
local function onActionKeyDown()
	log('D', "onActionKeyDown", "Pressed!")
end

local function onVehicleExtensionLoaded(vehID)
	log('D', "onVehicleExtensionLoaded", "Sending some data to the vehicle")

	local veh = be:getObjectByID(vehID) -- Если у вас нет ID, можно также использовать `be:getPlayerVehicle(0)`, чтобы получить текущий автомобиль.
	if not veh then return end -- Обычная проверка на ошибки

	local data = {
		["name"] = "Daniel W"
	}

	veh:queueLuaCommand("extensions.exampleVehicleExtension.onDataReceived('" .. jsonEncode(data) .. "')")
end

local function modifyMessage(message)
	message = message .. " [Modified!]"
	guihooks.trigger('MessageReady', message)
end

-- Экспорт интерфейса
--------------------------------------------
M.onExtensionLoaded = onExtensionLoaded
M.onExtensionUnloaded = onExtensionUnloaded

M.onActionKeyDown = onActionKeyDown
M.onVehicleExtensionLoaded = onVehicleExtensionLoaded
M.modifyMessage = modifyMessage

--[[ Другие функции могут включать:
	- onPreRender(dtReal, dtSim, dtRaw)
	- onUpdate(dtReal, dtSim, dtRaw)
	- onClientPreStartMission(levelPath)
	- onClientPostStartMission(levelPath)

	Чтобы найти все возможные функции, выполните поиск в `BeamNG.Drive/lua` по следующему выражению: `extensions.hook(`
--]]

return M

Обратите внимание, что очень важно вернуть переменную M (модуль) с необходимыми функциями внутри! Например, без строки M.modifyMessage = modifyMessage функция bngApi.engineLua('extensions.exampleMod.modifyMessage("' + $scope.message + '")') не сможет найти функцию modifyMessage()

ui\modules\apps\ExampleApp\app.css

#exampleAppContainer {
    width: 100%;
    height: 100%;

    display: flex;
    flex-direction: column;
    align-items: center;
    align-content: center;
}

#exampleAppContainer > * {
    margin: 0;
    padding: 0;
}

ui\modules\apps\ExampleApp\app.json

{
  "domElement": "<example-app></example-app>",
  "name": "Example App",
  "types": [
    "ui.apps.categories.debug"
  ],
  "description": "example-app",
  "css": {
    "left": "0px",
    "height": "auto",
    "width": "270px",
    "min-width": "200px",
    "min-height": "90px",
    "top": "0px"
  },
  "author": "Daniel W",
  "version": "0.1",
  "directive": "exampleApp"
}

Директива должна быть такой же, как в файле Javascript

Функции Javascript, предоставляемые BeamNG для UI-приложений

bngApi.engineLua("lua_path.function()")

Полезно для запуска функции lua с аргументами или без них

Функции Lua, предоставляемые BeamNG для UI-приложений

guihooks.trigger("EventName", Payload)

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

ВАЖНО : Иногда может случиться так, что имя события, которое вы используете, уже используется внутри чего-то другого, и это вызовет проблемы, поэтому, например, если ваше приложение называется Nickel, хорошей практикой будет называть каждое событие Angular как NKEventName вместо EventName.