[ LUA Tutorial ] How to create in-game GUI

Discussion in 'Utilities and programming' started by GregBlast, Sep 11, 2013.

  1. GregBlast

    GregBlast
    Expand Collapse

    Joined:
    Aug 12, 2013
    Messages:
    224
    Hello everyone,


    in this tutorial I will show you how to create some basic in-game graphical user interface using LUA code. I tried to make it as detailed as possible so it may appear quite long for a basic tutorial but it actually is pretty short due to the parts of code and screenshots included.




    Some of you that have searched the available LUA files may have noticed the following code in '[game path]\lua\system\main.lua':
    Code:
    function showAIGUI()
        local g = [[beamngguiconfig 1
    callback system AIGUICallback
    title system configuration
    
    
    
    
    container
      type = verticalStack
      name = root
    
    
    
    
    control
      type = chooser
      name = aimode
      description = AI Mode
      level = 1
      selection = ]] .. aiMode .. [[
    
    
    
    
      option = off Off
      option = player Chasing Player
      option = car0 Flee from player
    
    
    
    
    control
      type = doneButton
      icon = tools/gui/images/iconAccept.png
      description = Done
      level = 2
    
    
    
    
    ]]    
        --print(g)
        gameEngine:showGUI(g)
    end
    

    Well that's actually the code that displays the AI menu you get when pressing 'CTRL+T' (by default) which lets you choose the AI behaviour. If you check the keyboard input file (which contains the list of keyboard key mappings) you'll see that 'Ctrl+T' is mapped to call a LUA function named 'showAIGUI' (see '[game path]\scripts\client\inputmaps\keyboard.inputmap.cs', '%mm.bindSLuaCmd(keyboard, "ctrl t", "showAIGUI()", "");'). This example already tells us a lot about how to build some GUI using LUA.


    But lets make a little example of our own. As a simple test to start with we'll make a GUI that lets you enter some text and display it in the console when you press a button. The easiest way would be to just duplicate this inside of the 'main.lua' file but it would be wise to get used to using the module system already to avoid losing your work if that file is updated in a future release of the game.

    Making a basic module is pretty simple. First you should create a new file within '[game path]\lua\system\' with a '.lua' extension (lets say 'testGUI.lua' for this example). The parts related to the module you will need are as basic as follows:
    - define a new table that will represent the module itself
    - create some local functions and variables that the module will use
    - expose some of those functions / variables in the module table to make them visible to the outside world
    - return the module table

    Here is a basic example you can start with (we will be using it for our GUI sample):
    Code:
    --    This will be the module table
    local M            = {}
    
    --    A function that prints some text
    --    and prevent raising an error if it is 'nil'
    local function printText( text )
        print( "Your text was: " .. tostring( text ) )
    end
    
    --    Public interface
    --    Defines what is visible to the outside world
    M.printText        = printText
    
    --    Return the module table
    return M
    
    
    
    

    That's it for the base of our module. Now it needs to be made available from the 'main.lua' file because '-- this file is executed once on the engine lua startup' (as you can read at the top of it). A few lines below that one you'll notice a series of 'require' statements. Those are the ones that make the different modules available. Just as for 'console = require("console")' and other such statements add one for our module:
    Code:
    testGUI   = require("testGUI")
    Now you can run the game or if it was already started reload the LUA System by pressing 'Shift+T' (by default). If you new open your console (try either ' or ยด or `if you don't know which key opens it) you can test your module by calling any of its public members. For now we only defined the 'printText' function so if you type 'testGUI.printText( "Hello world !" )' it should output:
    Code:
    Your text was: Hello world !


    Alright now lets head to the GUI stuff.

    To create some functional GUI you need to do the following:
    - create the GUI elements => put them inside of a formatted string (surrounded with [[]])
    - display them => call 'gameEngine:showGUI' with your formatted string
    - create a callback function that will serve as an event handler for the controls => handle your controls behaviour within that function

    1. Defining the GUI elements
    From the pre-existing example we can assume that a nwe GUI must start with:
    Code:
    [[beamngguiconfig 1
    I'm not yet sure what the number refers to so just use 1 for now.

    You must then define its event callback and title:
    Code:
    [[beamngguiconfig 1
    callback system testGUI.GUICallback
    title Test GUI
    
    Note that 'testGUI.GUICallback' refers to a function you will need to declare later and that will need to be part of the public interface unless it's global.

    You then need to add a container to your window that will serve as the root control (client area). I don't yet know any other than 'verticalStack' so lets use that one:
    Code:
    [[beamngguiconfig 1
    callback system testGUI.GUICallback
    title Test GUI
    
    container
        type = verticalStack
        name = root
    
    
    Keep in mind that this string will be parsed by some other code which could potentially change in the future so a good idea would be to respect at least the line separations.

    We previously said we wanted to type some text so we can now add a control of type 'text'. Note that its 'name' property will be passed to the event handler as the variable holding the text you entered.
    Code:
    [[beamngguiconfig 1
    callback system testGUI.GUICallback
    title Test GUI
    
    
    container
        type = verticalStack
        name = root
        
    control
        type = text
        name = inputText
        description = Your text here
        level = 1
    
    I suppose the 'level' property is meant to somehow determine a hierarchical belonging but I couldn't see it in action yet.

    The 'description' property is the text that will be displayed next to the text box control as a label.

    Finally we will add a button that will do the job of printing our text into the console:
    Code:
    [[beamngguiconfig 1
    callback system testGUI.GUICallback
    title Test GUI
    
    
    container
        type = verticalStack
        name = root
        
    control
        type = text
        name = inputText
        description = Your text here
        level = 1
        
    control
        type = doneButton
        icon = tools/gui/images/iconAccept.png
        description = Ok
        level = 2
        
    ]]
    
    Even if a 'button' control does exist a 'doneButton' will close your window and pass a 'done = 1' value to your event handler. We'll get back to it soon.

    The 'icon' property determines the icon that will be displayed to the left of the button text. As you can see the game already contains a set of icons within the '[game path]\tools\gui\images' folder.


    2. Showing the GUI
    That makes the GUI configuration. To display it we need to pass it to 'gameEngine:showGUI'. Let's make a function that does that:
    Code:
    --    This is the function that displays the GUI
    --    It should be included in the public interface so that
    --    any external component can call it (like a key bind in keyboard mappings)
    local function showGUI()
        local g        = [[beamngguiconfig 1
    callback system testGUI.GUICallback
    title Test GUI
    
    
    container
        type = verticalStack
        name = root
        
    control
        type = text
        name = inputText
        description = Your text here
        level = 1
        
    control
        type = doneButton
        icon = tools/gui/images/iconAccept.png
        description = Ok
        level = 2
        
    ]]
        gameEngine:showGUI( g )
    end
    
    This function simply stores the text in a local variable 'g' which is then passed to 'gameEngine:showGUI'. That will do the job.

    As my comment says you need to make this function available for the outside world so if you remember your public interface that should still stand at the bottom of your file right above the return statement (if not move it there ;) just add:
    Code:
    M.showGUI        = showGUI

    Now if you reload your LUA System in game then open your console and type 'testGUI.showGUI()' (select 'BNGS' in the drop-down on the bottom left of the console as we're executing on 'SYSTEM' hence the 'S') you should see your GUI already. Pressing the 'Ok' button will close it.


    3. The event handler (callback)
    Now lets add the action part. Above your 'showGUI' function define another local function called 'GUICallback' that will take 2 parameters:

    - mode => represents the kind of action that happened
    -> 'apply' is passed after selecting something in a list or clicking a 'doneButton' (and probably other control actions that I don't yet know
    -> 'selectStart' is passed when a list is opened
    -> 'selectEnd' is passed when a list is closed (actually maybe both at both times, to be checked)
    -> 'button' is passed when a 'button' is clicked
    -> and for the rest I don't know yet

    - str => a string representation of the event arguments serialized
    -> they can easily be deserialized using the 'unserialize' global function

    So if you got it right after pressing our 'Ok' button we will end up in our 'GUICallback' function with a mode set to 'apply'. As I mentioned earlier the arguments will also contain an 'inputText' variable which corresponds to the name we gave to our text box and that will be holding our text value. So if you still have the 'printText' function we defined earlier you can define your 'GUICallback' function like the following:
    Code:
    --    This is the callback that is called once a GUI event is triggered (button click, ...)
    local function GUICallback( mode, str )
        
        --    Extract args
        local args    = unserialize( str )
        
        --    Clicked the Ok button
        if mode == "apply" then
            printText( args.inputText )
        end
    end
    
    We first deserialize the arguements which gives us a table holding some variables given by the controls we have (actually some will automatically provide values to an event fired by a 'doneButton', like the 'text' controls with their 'name' property. But if you define another 'button' control with a name property that one will not appear in the arguments received when clicking the 'doneButton').

    We then check that the event mode is "apply" and if so we call our 'printText' function with the 'inputText' variable from the event arguments table.

    Not that you can use the global function 'dump' by passing it the 'args' table to inspect the contents of the table. It will output the table contents in the console.

    Don't forget to add the callback to your public interface otherwise it won't be called as the GUI string references it from there.
    Code:
    M.GUICallback    = GUICallback

    That's it for this first GUI tutorial. If you reload your LUA System (Shift+T) and open the GUI ('testGUI.showGUI()' in console) then enter some text in the text box and hit the 'Ok' button you should get 'Your text was: ' followed by the text you entered in console.

    Tutorial-LUA-BasicGUI_001.jpg
    Tutorial-LUA-BasicGUI_002.png

    Also note that it seems that only one LUA GUI can be displayed at a time. At least using the parameters from our example. If you had opened the AI GUI prior it will be closed once you open this one.

    You can find below a fresh copy of the file with the whole code.


    Feel free to comment on this post and let me know what you think about it. Any opinion is welcome :).


    PS: you can easily bind a keyboard shortcut to your GUI by adding a line to the '[game path]\scripts\client\inputmaps\keyboard.inputmap.cs' file. For example you can define '%mm.bindSLuaCmd(keyboard, "ctrl u", "testGUI.showGUI()", "" );' to use 'Ctrl+U' as a shortcut. 'bindSLuaCmd' allows you to give any (well there could be some restrictions ;)) LUA string as the shortcut command to execute knowing the fact that it will be executed on 'SYSTEM LUA' (so anything available from 'main.lua' as discussed earlier).


    Thanks for reading :)
     

    Attached Files:

    #1 GregBlast, Sep 11, 2013
    Last edited: Sep 11, 2013
    • Like Like x 1
  2. tdev

    tdev
    Expand Collapse
    Developer
    BeamNG Team

    Joined:
    Aug 3, 2012
    Messages:
    3,031
    nice write up and good to see you could find your way through our code :)
     
  3. FluffyWoofles

    FluffyWoofles
    Expand Collapse

    Joined:
    Sep 8, 2013
    Messages:
    10
    Thats awesome, Could you maybe make an RP and MPH gauge? I would love that. I would try it myself but last time i messed with the .lua file i almost wrecked my game D:
     
  4. GregBlast

    GregBlast
    Expand Collapse

    Joined:
    Aug 12, 2013
    Messages:
    224
    @FluffyWoofles
    I described it with a lot of details as you can see and as you will read you would create a separate module without having to really mess with much of the default code. The only modification that needs to be done to the existing code is a line that makes the module available for the 'LUA System'.
    As for the gauges and stuff I don't yet know (if possible) how to display (and rotate...) images in a GUI.

    @tdev
    I'll make another tutorial probably later today showing how to make a little more advanced GUI as the one I made for Incognito and his waypoints. But I have plenty of questions for you tdev as you may have guessed ;). Like would you have a list of the available controls and their properties ? How can you have multiple GUIs open at the same time (if possible) ? And so on...
     
    #4 GregBlast, Sep 11, 2013
    Last edited: Sep 11, 2013
  5. bits&bytes

    bits&bytes
    Expand Collapse

    Joined:
    Jun 13, 2013
    Messages:
    40
    Thanks for this clear explanation GregBlast.

    This is just what I need.
    I was messing with the keyboard steering but had to restart the game after every parameter change.
    Now i can make a GUI to change some parameters in game.
    unfortunately i have not much spare time at the moment. But when I have more time, I will take a close look at this.

    I also beginning to find my way trough the code, but it is harder then I thought.

    A gauge that display's the speed in Km/h is also something that I had in mind, but had no idea where to start.
    Maybe it is possible with a simple GUI, but I do not know if values on the GUI will be updated while it is open on screen.
     
  6. Incognito

    Incognito
    Expand Collapse

    Joined:
    Aug 4, 2013
    Messages:
    246
    An example of drawing different "sensors" can be viewed in lua\vehicle\canvas.lua . But it only works when the debug mode is enabled (it is not very convenient). Btw, Simple-Speedometer-lua-scripting
     
  7. GregBlast

    GregBlast
    Expand Collapse

    Joined:
    Aug 12, 2013
    Messages:
    224
    But from System you can:
    • open [game path]\lua\system\main.lua
    • below the commented initCanvas() just add scanvas.initCanvas()
    • in the graphicsStep function uncomment scanvas.update()
    • open [game path]\lua\system\scanvas.lua
    • on the public interface definition (bottom of file, before return) add M.initCanvas = initCanvas
    • for a test, in the update function comment drawRacingScreen(dt, modeNumber) and uncomment drawTestScreen(dt, modeNumber)
    • in game, press shift + T to reload System LUA and you'll get a black rectangle with diagonals and some text inside
    • see drawTestScreen function for what is drawn and initCanvas for the default configuration (text font and forecolor, rectangle color, ...)
    • the list of SkPaint instances stored within the paints table are defined in initCanvas and are the available drawing configurations
    • in drawTestScreen replace paints.border with paints.red to get a red rectangle instead of black

    This doesn't get drawn on debug screens just the normal one. If you can get the information you need from System then you can draw it there (or a separate module ;)).
     
  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice