Files
Docs/docs/fr/beamng/dev/modding/ui-apps.md
T

12 KiB

!!! warning "Ce site est en cours de construction !"

Ce site est actuellement en cours de développement.

Vous pensez pouvoir contribuer ? Cliquez simplement sur l'icône en forme de crayon située à droite de la page !

Vous pouvez également contribuer à n'importe quelle autre page.

Création d'une UI-App

Pour créer une UI-App, vous devez avoir quelques connaissances du framework AngularJS. La documentation principale est disponible ici : documentation AngularJS.

Structure des fichiers

Une UI-App fonctionnelle nécessite quatre fichiers principaux :

  • app.js — Contient le code JavaScript principal de l'UI-App. Documentation JavaScript
  • app.html — Contient le code HTML qui affiche l'application. Documentation HTML
  • app.json — Contient les informations et la configuration de l'UI-App.
  • app.png — Image affichée dans le sélecteur d'UI-Apps.

Style de l'UI-App

Il est recommandé d'utiliser une balise <style> directement dans le fichier HTML pour styliser votre application. Un fichier .css fonctionne également, mais les modifications ne seront pas visibles en temps réel.


Exemple

Cet exemple provient de DanielW. Merci à lui !

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>Rapport : <span>{{ gearName }}</span></span>

        <div layout="row" layout-align="center center">
            <md-input-container flex>
                <label>Message</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()">Envoyer</md-button>
        </div>

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

        <!-- Zone de défilement -->
        <ul bng-nav-scroll style="margin: 0; padding: 0; overflow-y: auto; width: 100%; height: 100%; background-color: #37373740;">

            <!-- Parcourt les messages et les affiche -->
            <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>

                <!-- Bouton permettant de supprimer le message.
                     Appelle la fonction deleteMessage() dans 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>

Ici, vous pouvez voir :

  • une balise <span> affichant le rapport du véhicule ;
  • un champ de saisie permettant d'envoyer un message à la fonction sendMessage() du JavaScript ;
  • une balise <li> répétée grâce à ng-repeat, utilisant la variable messages définie dans le 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 = []

            // Configure les flux de données dont nous avons besoin.
            // Ici, nous utilisons uniquement les informations du moteur.
            let streamList = ['engineInfo']
            StreamsManager.add(streamList)

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

            $scope.$on('streamsUpdate', function(event, streams) {
                if (!streams.engineInfo)
                    return

                // lua/vehicle/controller/vehicleController.lua:538
                // (ou utilisez console.log pour effectuer vos propres tests)
                let gear = streams.engineInfo[5]

                // Met à jour le rapport affiché dans le HTML si nécessaire
                if ($scope.gearName !== gear)
                    $scope.gearName = gear
            })

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

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

                // Envoie le message à l'extension Lua pour le modifier
                bngApi.engineLua(
                    'extensions.exampleMod.modifyMessage("' +
                    $scope.message +
                    '")'
                )

                $scope.message = ''
            }

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

            // La fonction modifyMessage() déclenchera cet événement
            // avec le message modifié.
            $scope.$on('MessageReady', function(_, modifiedMessage) {
                $scope.messages.push(modifiedMessage)
            })
        }]
    }
}])

Le fonctionnement de $scope

L'utilisation de $scope est particulièrement importante.

Vous devez définir vos variables et vos fonctions dans $scope afin qu'elles puissent être utilisées depuis le HTML via les directives ng-*.

Par exemple :

$scope.message = ''
$scope.messages = []

$scope.sendMessage = function() {
    // ...
}

Le HTML peut alors accéder directement à ces éléments :

<input ng-model="message">

ou :

<md-button ng-click="sendMessage()">
    Envoyer
</md-button>

Dans cet exemple, lorsque sendMessage() est exécutée depuis le HTML, elle envoie le message vers une extension Lua située dans le dossier extensions du mod afin d'exécuter la fonction modifyMessage().


Communication entre JavaScript et Lua

Voici à quoi peut ressembler la partie Lua :

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

Il s'agit ici d'une version simplifiée permettant de comprendre le fonctionnement.

L'élément important est :

guihooks.trigger('MessageReady', message)

guihooks.trigger() déclenche un événement AngularJS qui peut être récupéré avec :

$scope.$on('MessageReady', function(_, modifiedMessage) {
    $scope.messages.push(modifiedMessage)
})

Le fonctionnement est donc :

HTML
  ↓
sendMessage()
  ↓
bngApi.engineLua()
  ↓
Lua : modifyMessage()
  ↓
guihooks.trigger()
  ↓
JavaScript : $scope.$on()
  ↓
$scope.messages
  ↓
ng-repeat
  ↓
Affichage dans l'UI-App

Extension Lua complète

lua\ge\extensions\exampleMod.lua

local M = {}

--[[
    Point d'entrée de notre extension.
    C'est ce que le jeu charge depuis notre modScript.lua.

    Dans ce fichier, nous pouvons communiquer avec :
      1. Notre extension du véhicule.
      2. Les entrées utilisateur.
]]

-- Hooks de l'extension
--------------------------------------------

local function onExtensionLoaded()
    log('D', "onExtensionLoaded", "Appelée")
end

local function onExtensionUnloaded()
    log('D', "onExtensionUnloaded", "Appelée")
end

-- Fonctions personnalisées
--------------------------------------------

local function onActionKeyDown()
    log('D', "onActionKeyDown", "Touche pressée !")
end

local function onVehicleExtensionLoaded(vehID)
    log('D', "onVehicleExtensionLoaded", "Envoi de données au véhicule")

    local veh = be:getObjectByID(vehID)

    -- Si vous n'avez pas l'ID, vous pouvez également utiliser :
    -- 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 .. " [Modifié !]"
    guihooks.trigger('MessageReady', message)
end

-- Interface publique
--------------------------------------------

M.onExtensionLoaded = onExtensionLoaded
M.onExtensionUnloaded = onExtensionUnloaded

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

--[[
    D'autres fonctions peuvent notamment être :

      - onPreRender(dtReal, dtSim, dtRaw)
      - onUpdate(dtReal, dtSim, dtRaw)
      - onClientPreStartMission(levelPath)
      - onClientPostStartMission(levelPath)

    Pour trouver les différents hooks disponibles,
    recherchez "extensions.hook(" dans :

    BeamNG.Drive/lua
]]

return M

Il est très important de retourner la variable M contenant les fonctions nécessaires.

Par exemple, sans :

M.modifyMessage = modifyMessage

l'appel JavaScript :

bngApi.engineLua(
    'extensions.exampleMod.modifyMessage("' + $scope.message + '")'
)

ne pourra pas trouver la fonction modifyMessage().


CSS de l'UI-App

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;
}

Configuration de l'UI-App

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"
}

La propriété directive doit correspondre exactement à la directive définie dans le fichier JavaScript :

.directive('exampleApp', [function() {

Dans cet exemple :

app.json
    ↓
"directive": "exampleApp"
    ↓
app.js
    ↓
.directive('exampleApp', ...)

Fonctions JavaScript fournies par BeamNG

Pour exécuter une fonction Lua depuis une UI-App, utilisez :

bngApi.engineLua("chemin.lua.fonction()")

Cette fonction est particulièrement utile pour exécuter une fonction Lua avec ou sans arguments.

Par exemple :

bngApi.engineLua(
    'extensions.exampleMod.modifyMessage("Bonjour !")'
)

Fonctions Lua fournies par BeamNG pour les UI-Apps

Pour envoyer un événement vers l'interface JavaScript :

guihooks.trigger("EventName", Payload)

Le Payload peut être de différents types. Il est cependant recommandé d'utiliser un tableau, un objet ou une chaîne de caractères afin d'éviter les problèmes de transmission des données.

Exemple :

guihooks.trigger("MessageReady", "Bonjour !")

Puis, côté JavaScript :

$scope.$on('MessageReady', function(_, message) {
    console.log(message)
})

!!! warning "Attention aux noms d'événements"

Il est possible que le nom d'événement que vous utilisez soit déjà utilisé en interne par BeamNG.drive.

Cela peut provoquer des conflits avec d'autres systèmes du jeu.

Par exemple, si votre application s'appelle `Nickel`, il est préférable de préfixer vos événements :

```text
NKEventName
NKMessageReady
NKUpdate
```

plutôt que d'utiliser des noms génériques comme :

```text
EventName
MessageReady
Update
```

Utiliser un préfixe propre à votre application permet de réduire fortement les risques de conflits.