Hello everyone, Introduction In this tutorial I will demonstrate how to create a GUI that updates itself based on user interaction. For this tutorial you must already know how to do what I described in my first tutorial about GUIs: How to create in-game GUI. Getting started For this tutorial we will need most of the things that were made in the previous tutorial. So we can start a new module with the same contents already. Under [game path]\lua\system create a new file called dynamicGUI.lua and copy the contents of the file testGUI.lua from the previous tutorial (available here How to create in-game GUI). As you should remember don't forget to add a require statement for this new module into [game path]\lua\system\main.lua. Below the latest require statement add: Code: dynamicGUI = require("dynamicGUI") Now the module is already functional. One thing to note though is that in the previous tutorial we defined the callback of the controls with... Code: callback system testGUI.GUICallback ...which means that if you run this module as is any user interaction on the GUI will call the callback from the module testGUI. As it's not what we want you can change this to Code: dynamicGUI.GUICallback . So now if you reload the System LUA in game (Shift+T by default) and open your console (with mode BNGS selected) and type in dynamicGUI.showGUI() you should have the exact same behaviours as in the previous tutorial. Preparing our GUI Alright first we need to get rid of the things we won't need. Start by removing the printText function and its exposition in the public interface: Code: M.printText = printText Also remove the call to it in the GUICallback function. We'll be using a text control to change the title of the GUI. So in the showGUI function leave the inputText control of type text but change its name to txtTitle and its description to Title. Code: control type = text name = txtTitle description = Title The doneButton will serve as a close button. So change its icon to iconCancel.png and its description to Close. Code: control type = doneButton icon = tools/gui/images/iconCancel.png description = Close We'll need a button to validate the input info without closing the GUI. So add a button above the doneButton with icon iconAccept.png and Ok for description. Give it the name btOk. We'll also allow the user to change the description of this button. So this time add another text control below the txtTitle control with the name txtOk and description Button Ok text. At this point your code should look like this (the callback has been cleared): Code: -- This will be the module table local M = {} -- 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 ) end -- 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 dynamicGUI.GUICallback title Test GUI container type = verticalStack name = root control type = text name = txtTitle description = Title control type = text name = txtOk description = Button Ok text control type = button name = btOk icon = tools/gui/images/iconAccept.png description = Ok control type = doneButton icon = tools/gui/images/iconCancel.png description = Close ]] gameEngine:showGUI( g ) end -- Public interface -- Defines what is visible to the outside world M.showGUI = showGUI M.GUICallback = GUICallback -- Return the module table return M And here's what you should get in-game: Adding the actions Now lets head to the control actions. As I said we'll want to change the title of the GUI and the text on the button Ok. That's already two variables we can define in our module. So at the top of the file below the module declaration add the following variables: Code: local title = "Dynamic GUI" local okText = "Ok" We can already use them in our GUI definition in the showGUI function by concatenating the value of these variables. For this you can erase the text and close the formatted string there then use .. to concatenate it with some other string and then use the variable name. Then concatenate it with a formatted string opening using .. [[: Code: local function showGUI() local g = [[beamngguiconfig 1 callback system dynamicGUI.GUICallback title ]] .. title .. [[ container type = verticalStack name = root control type = text name = txtTitle description = Title control type = text name = txtOk description = Button Ok text control type = button name = btOk icon = tools/gui/images/iconAccept.png description = ]] .. okText .. [[ control type = doneButton icon = tools/gui/images/iconCancel.png description = Close ]] gameEngine:showGUI( g ) end Now lets make it change on user input. Head to the GUICallback function and handle the button click event by checking that mode is equal to button: Code: -- Clicked a button if mode == "button" then end Remember than when a button is clicked we have an argument called button with the button name in it. We can thus check which button was pressed. If it's the Ok button we will simply change the value of our title variable and the value of the okText one. We'll also check that the text entered is not empty so we don't assign an empty text to the title or button. Here's what the callback should look like with this done: Code: local function GUICallback( mode, str ) -- Extract args local args = unserialize( str ) -- Clicked a button if mode == "button" then -- Button: Ok if args.button == "btOk" then -- Update title if args.txtTitle and args.txtTitle ~= "" then title = args.txtTitle end -- Update button Ok text if args.txtOk and args.txtOk ~= "" then okText = args.txtOk end end end end Well if you click the ok button now you won't notice any change. You'll have to close the GUI and re-open it to see your changes. Dynamically updating the GUI To update our GUI and see its changes immediately we can either close it and re-open it by hand... or we can do this by code. For this you can simply call your showGUI function from within your callback function. You may be tempted to simply add showGUI() after changing the title and Ok text but you'd be wrong. For the callback here the function showGUI hasn't yet been defined. And as it is not global by the time it is called it won't be available. To avoid your GUI from raising an error on calling showGUI() you can move your showGUI function above the GUICallback function. Or you can simply use the module table as a prefix with M.showGUI(): Code: local function GUICallback( mode, str ) -- Extract args local args = unserialize( str ) -- Clicked a button if mode == "button" then -- Button: Ok if args.button == "btOk" then -- Update title if args.txtTitle and args.txtTitle ~= "" then title = args.txtTitle end -- Update button Ok text if args.txtOk and args.txtOk ~= "" then okText = args.txtOk end end -- Reload GUI M.showGUI() end end Now when you press the Ok button your title and Ok text will change according to what you entered. You just may be surprised with some specific input not showing formatted like you wanted. For example if you enter Done you will get done. This must be because those words are pre-formatted and used like that. Adding optional controls While we're at it I thought it would be even nicer to have an Options button showing or hiding our text controls. So for that we have to make the part of the GUI that defines the text controls optional. Let's put that part in a separate function for clarity. Add a function called addOptionsGUI above showGUI and for now define it to always return the text controls as we have that already. Code: -- Adds the option controls to the GUI local function addOptionsGUI() return [[ control type = text name = txtTitle description = Title control type = text name = txtOk description = Button Ok text ]] end Don't forget to update the showGUI function to use this as the method that will define the options controls. Replace them with a call to the addOptionsGUI function: Code: local function showGUI() local g = [[beamngguiconfig 1 callback system dynamicGUI.GUICallback title ]] .. title .. [[ container type = verticalStack name = root ]] .. addOptionsGUI() .. [[ control type = button name = btOk icon = tools/gui/images/iconAccept.png description = ]] .. okText .. [[ control type = doneButton icon = tools/gui/images/iconCancel.png description = Close ]] gameEngine:showGUI( g ) end Well these controls are supposed to be optional so lets make them optional. First we need a boolean variable to serve as a flag to determine whether or not we want those text controls in the GUI. In the variables declaration at the top of the file add one called showOptions and initialize it to false: Code: local showOptions = false Now update the new addOptionsGUI function so that it returns an empty string if showOptions is false: Code: local function addOptionsGUI() if not showOptions then return "" end return [[ control type = text name = txtTitle description = Title control type = text name = txtOk description = Button Ok text ]] end Add an options button as first control of your GUI by updating the showGUI function and make its icon change whether it is supposed to be pressed or not. For this you can concatenate a check on our showOptions flag to decide which icon to use: Code: control type = button name = btOptions icon = tools/gui/images/]] .. ( showOptions and [[iconDelete.png]] or [[iconAdd.png]] ) .. [[ description = Options Now the only thing left is to switch our flag when this button is pressed. Update your callback function to check for a click on the btOptions button and toggle the showOptions variable if so. Don't forget we still need to refresh the GUI with that so leave the call to M.showGUI() outside of the button name tests (but inside the mode == "button" test so that other events do not reload the GUI): Code: local function GUICallback( mode, str ) -- Extract args local args = unserialize( str ) -- Clicked a button if mode == "button" then -- Button: Options if args.button == "btOptions" then -- Toggle options showOptions = not showOptions -- Button: Ok elseif args.button == "btOk" then -- Update title if args.txtTitle and args.txtTitle ~= "" then title = args.txtTitle end -- Update button Ok text if args.txtOk and args.txtOk ~= "" then okText = args.txtOk end end -- Reload GUI M.showGUI() end end That's it ! Click the Options button to diplay your text controls and click it again to hide them. One last thing that is optional but pretty user-friendly is to automatically hide the text controls once we press the Ok button. Just set showOptions to false at the end of the args.button == "btOk" test: Code: -- Hide options showOptions = false Screenshots Here are some screenshots of the final GUI in action: Thanks for reading As usual any comments on mistakes or possible improvements are welcome . Everything I described here may not be the best way to do things so let me know about it. Full codeThe full code can be found below this in the attachments. The zip file contains only the dynamicGUI.lua file (the module file) which should be dropped within [game path]\lua\system.
I am trying to make a GUI for changing the steering rate parameters, but I get an error: “attempt to call global ‘UpdateSteeringParameters’ (a nil value)." This is the GUI, the line where I get the error from is marked red ([gamepath]\lua\system\SteeringParametersGUI.lua). Code: -- Module add by bits&bytes on 19/Sep/2013 ------------------------------------------ -- 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 -- 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.Parameter_kbdInRate ) printText( tostring(str) ) Par_kbdInRate = tonumber( args.Parameter_kbdInRate ) print("Par_kbdInRate = " .. tostring(Par_kbdInRate)) [COLOR=#ff0000]UpdateSteeringParameters(Par_kbdInRate)[/COLOR] end end -- 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() print("GUI wordt aangemaakt") print("Par_kbdInRate = " .. tostring(Par_kbdInRate)) local g = [[beamngguiconfig 1 callback system SteeringParametersGUI.GUICallback title Steering Parameters container type = verticalStack name = root control type = text name = Parameter_kbdInRate description = Steering In Rate (original was ]] .. tostring(Par_kbdInRate) .. [[) control type = doneButton icon = tools/gui/images/iconAccept.png description = Ok ]] gameEngine:showGUI( g ) end -- Public interface -- Defines what is visible to the outside world M.printText = printText M.showGUI = showGUI M.GUICallback = GUICallback -- Return the module table return M The function I am trying to launch is programmed in input.lua (under [gamepath]\lua\vehicle\). The function is marked blue. Code: -- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt local M = {} M.keys = {} -- TODO: REMOVE M.rawDevices = {} M.raw = {} M.state = {} M.VALUETYPE_KEYBD = 0 M.VALUETYPE_PAD = 1 M.VALUETYPE_DIRECT = 2 local rateMult = nil --set initial rates (derive these from the menu options eventually) local kbdInRate = 8.0 local kbdOutRate = 4.0 local kbdAutoCenterRate = 3.0 Par_kbdInRate = 1 -- Mod by bits&bytes on 19/sep/2013 Par_kbdOutRate = 1 -- Mod by bits&bytes on 19/sep/2013 Par_kbdAutoCenterRate = 1 -- Mod by bits&bytes on 19/sep/2013 print ("Par_kbdInRate = " .. tostring(Par_kbdInRate)) -- Mod by bits&bytes on 19/sep/2013 [COLOR=#0000ff]function UpdateSteeringParameters(var1) print("Parameters worden geupdate: " .. var1) end [/COLOR] local padInRate = 5.0 local padOutRate = 2.5 local steerSmoothing = newExponentialSmoothing(4) local function init() --scale rates based on steering wheel degrees if hydros then for _, h in pairs (hydros.hydros) do --check if it's a steering hydro if h.inputSource == "steering_input" then --if the value is present, scale the values if h.steeringWheelLock then rateMult = 450 / math.abs(h.steeringWheelLock) break end end end end if rateMult == nil then rateMult = 5/8 end --inRate (towards the center), outRate (away from the center), autoCenterRate, startingValue M.state = { axisx0 = { val = 0, inputType = 0, smootherKBD = newTemporalSmoothing(kbdInRate * rateMult, kbdOutRate * rateMult, kbdAutoCenterRate * rateMult, 0), smootherPAD = newTemporalSmoothingNonLinear(padInRate * rateMult, padOutRate * rateMult, nil, 0), minLimit = -1, maxLimit = 1, binding = "steering" }, axisy0 = { val = 0, inputType = 0, smootherKBD = newTemporalSmoothing(3, 3, 5, 0), smootherPAD = newTemporalSmoothing(10, 10, nil, 0), minLimit = 0, maxLimit = 1, binding = "throttle" }, axisy1 = { val = 0, inputType = 0, smootherKBD = newTemporalSmoothing(3, 3, 5, 0), smootherPAD = newTemporalSmoothing(10, 10, nil, 0), minLimit = 0, maxLimit = 1, binding = "brake" }, axisy2 = { val = 0, inputType = 0, smootherKBD = newTemporalSmoothing(3, 3, 5, 0), smootherPAD = newTemporalSmoothing(10, 10, nil, 0), minLimit = 0, maxLimit = 1, binding = "parkingbrake" }, axisy3 = { val = 0, inputType = 0, smootherKBD = newTemporalSmoothing(3, 3, 5, 0), smootherPAD = newTemporalSmoothing(10, 10, nil, 0), minLimit = 0, maxLimit = 1, binding = "clutch" }, } end local function update(dt) -- map the values for k, e in pairs(M.state) do local tmp = 0 if e.inputType == M.VALUETYPE_DIRECT then -- steering wheel tmp = e.val else if e.inputType == M.VALUETYPE_PAD then -- joystick / game controller - smoothing without autocentering tmp = e.smootherPAD:get(e.val, dt) else -- VALUETYPE_KEYBD -- digital - smoothing with autocenter tmp = e.smootherKBD:get(e.val, dt) end if k == "axisx0" then tmp = steerSmoothing:get(tmp) end end M[e.binding] = tmp tmp = math.min(math.max(tmp, e.minLimit), e.maxLimit) electrics.values[e.binding..'_input'] = tmp end end -- deviceInst : Device instance: joystick0, joystick1, etc -- fValue : Value typically ranges from -1.0 to 1.0, but doesn't have to. - It depends on the context. -- fValue2, fValue3, fValue4 : Extended float values (often used for absolute rotation Quat) -- iValue : Signed integer value -- action : InputActionType -- deviceType : InputDeviceTypes -- objType : InputEventType -- objInst : InputObjectInstances -- ascii : ASCII character code if this is a keyboard event. -- modifier : Modifiers to action: SI_LSHIFT, SI_LCTRL, etc. local function processRawEvent(deviceInst, fValue, fValue2, fValue3, fValue4, iValue, action, deviceType, objType, objInst, ascii, modifier) --print("InputEvent("..deviceInst..", "..fValue..", "..fValue2..", "..fValue3..", "..fValue4..", "..iValue..", "..action..", "..deviceType..", "..objType..", "..objInst..", "..ascii..", "..modifier..")") local dev = deviceType..deviceInst if M.raw[dev] == nil then M.raw[dev] = {} end if M.raw[dev][objType] == nil then M.raw[dev][objType] = {} end if M.raw[dev][objType][objInst] == nil then M.raw[dev][objType][objInst] = {} end local d = M.raw[dev][objType][objInst] d.fValue = fValue d.fValue2 = fValue2 d.fValue3 = fValue3 d.fValue4 = fValue4 d.iValue = iValue d.action = action --dump(M.raw) end local function mapsReloaded() --print "input maps were reloaded:" dump(M.rawDevices) --[[ if bdebug.mode == 10 then -- we are currently in beam debug mode, what coincidence ;) canvas.inputInfoText = "Thanks, but this features is not implemented yet." -- TODO: WIP end ]]-- end local function reset() M.raw = {} steerSmoothing:reset() for k, e in pairs(M.state) do e.smootherKBD:reset() e.smootherPAD:reset() end end local function event(itype, ivalue, inputType) --print("input.event("..tostring(itype)..","..tostring(ivalue)..","..tostring(inputType)..")") M.state[itype].val = ivalue M.state[itype].inputType = inputType end local function toggleEvent(itype, ivalue, inputType) --print("input.toggleEvent("..tostring(itype)..","..tostring(ivalue)..","..tostring(inputType)..")") if ivalue == 0 then return end if M.state[itype] ~= nil and M.state[itype].val > 0.5 then M.state[itype].val = 0 M.state[itype].inputType = 0 else M.state[itype].val = 1 M.state[itype].inputType = 0 end end local function toggleParkingbrake() if M.parkingbrakeInput < 0.01 then M.parkingbrakeInput = 1 elseif M.parkingbrakeInput > 0.99 then M.parkingbrakeInput = 0 end end -- public interface M.update = update M.init = init M.reset = reset M.toggleParkingbrake = toggleParkingbrake M.processRawEvent = processRawEvent M.mapsReloaded = mapsReloaded M.toggleDynamicSteering = toggleDynamicSteering M.event = event M.toggleEvent = toggleEvent return M I am planning to call the local function Init() from there with the new steering parameters from the GUI. What am I doing wrong? By the way, thanks for making this detailed tutarial.
bits&bytes, system lua and vehicle lua work separately from each other. And as far as I know, currently they cant "talk" to each other.
Yes and I personally didn't yet have a look at how the GUIs interacting with cars (like the configurator) work with the vehicles. But that would be the thing I would dig into. But can you describe what you're trying to get as a final result ? Because you can get some values of the current car and trigger some input events from System LUA via 'queueLuaCommand'. If your module has an 'update' function called from the 'graphicsStep' function of 'main.lua' than you can modify your steering from that 'update' function which will be called quite often (having your chosen "rates" chosen from the GUI and stored within some local variables of the module and used within 'update'). @Incognito I'm not yet sure whether this is a viable solution or not as I haven't tried yet. Maybe Incognito can give us a hint on that ? - - - Updated - - - Double post because both are different. Actually I just found out that when you use 'queueLuaCommand' with a 'BeamObject' retrieved with 'BeamEngine:getSlot(car id number, 0 for player)' you can use the vehicle lua. Which means the vehicle modules can be used as they are (which is why you can call 'input.event' from 'queueLuaCommand'). In your case using the following snippet inside your System module should do the work: Code: local playerCar = BeamEngine:getSlot(0) playerCar:queueLuaCommand("input.UpdateSteeringParameters()") Just include your parameters taking care of the fact it's within a string . And Incognito seriously you didn't know about that ? Or you just faked it ? Because you're using it... PS: btw make your function LOCAL and add it to the public interface. So it's part of the module and not global to everything. EDIT: Just tested and it worked fine. But you should make a separate module. Rather than using 'input.lua' you can make one that requires it if needed.
I forgot about it function, sorry. just long ago I didn't write any mods for beamng. Btw, player car id does not always 0.
That explains a lot. I have already tried to move the GUI to the 'vehicle' part, with the require in [gamepath]\lua\vehicle\input.lua The GUI was displayed correct but the apply function was giving an error. I should also take a look at the car configurating GUI's. Do not know yet if they are at the vehicle part. But the answer from GregBlast looks also interesting to have a look at.
What error? Make UpdateSteeringParameters public and call it so: input.UpdateSteeringParameters(param)
You may want to have a look at the little example I made here: Speed limiter with GUI I made this after chatting here to have a concrete example. It forces you to no longer accelerate if you reach a speed that you can define with a GUI. So it checks your speed and inputs and acts according to those facts. I hope it will help.
(This part of the post is referenced to the previous tutorial How to create in-game GUI) Creating a GUI into the vehicle part of the lua code To have a GUI at het vehicle part of the lua code, you can do it the same way as in this (and the previous) tutorial with some minor changes: Save the GUI script under [game path]\lua\vehicle Place the ‘require’ statement into [game path]\lua\vehicle\default.lua: Code: -- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt -- change default lookup path package.path = "lua/vehicle/?.lua;lua/?.lua;?.lua" local STP = require "StackTracePlus" debug.traceback = STP.stacktrace require("utils") canvas = require("canvas") drivetrain = require("drivetrain") sounds = require("sounds") bdebug = require("bdebug") input = require("input") props = require("props") beamng = require("beamng") particlefilter = require("particlefilter") particles = require("particles") material = require("material") electrics = require("electrics") json = require("json") beamstate = require("beamstate") sensors = require("sensors") bullettime = require("bullettime") thrusters = require("thrusters") hydros = require("hydros") inputwizard = require("inputwizard") perf = require("perf") partmgmt = require("partmgmt") -- do not change its name, the GUI callback will break otherwise [COLOR=#0000ff]SteeringParametersGUI = require("SteeringParametersGUI") -- Mod by bits&bytes on 19/sep/2013[/COLOR] --console = require("console") -- globals for this object v = beamng.newVehicle() 3. Into the GUI script, replace the keyword ‘system’ by ‘activeVehicle’ (after ‘callback’ into the local function showGUI() ): 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 -- 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 -- 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 [COLOR=#0000ff]activeVehicle[/COLOR] testGUI.GUICallback title Test GUI container type = verticalStack name = root control type = text name = inputText description = Your text here control type = doneButton icon = tools/gui/images/iconAccept.png description = Ok ]] gameEngine:showGUI( g ) end -- Public interface -- Defines what is visible to the outside world M.printText = printText M.showGUI = showGUI M.GUICallback = GUICallback -- Return the module table return M ____________________________________________________________________________________________________________________________________ (This part of the post is about my modification for changing some steering parameters using a GUI) Are you guys talking about the same thing? Can you tell me how to do it? Btw, that error is soved (see step 3 above). Sorry for all the noob questions but I am new to Lua and realy appreciate yours help and advices. I reached the point of giving it all up about modding and that’s why I putted these codes full of rubbish into this tread. Incognito’s reaction was enough to switch on the light bulb again. I did not yet take a look into GregBlast’s reactions about passing variables between ‘vehicle’ and ‘system’, but I am sure I am going to need it someday in the future. For now I solved it by placing the GUI on the ‘vehicle’ part. Got my mod working now . As soon as I can make that function local and added to the public, I will start a WIP topic about this mod.
Code: local function someFunc() print("olololo") end -- public interface M.someFunc = someFunc Maybe the error was gone due to the fact that you no longer calls the function "UpdateSteeringParameters" after press "Apply" button? You can execute some lua-code (function) for the some car.
Thanks, I will check it out soon. No , the error was there because I used the keyword 'system' in stead of 'activeVehicle' into a GUI at the vehicle part. The codes in my previous post had nothing to do with my modification, they are the ones from GregBlast's tutorial with some minor changes to show the differences between GUI's on the system and on the vehicle site. It actualy references to the first tutorial about GUIs: How to create in-game GUI Sorry for all the confusion, i will do some editing and place some links to make it more clear.