GUI Tutorial
This page will teach you how to create your first own UI. The second part of this page will dive deeper into the possibilities of Arma 3 UIs.
The Basics
Terminology
Before we begin let us clear up some words and their meaning:
Term | Meaning |
---|---|
UI | User Interface. What the player will see. Also: GUI (Graphical User Interface, IGUI (meaning not entierly clear, used for HUDs in Arma), display, dialog. |
Dialog/Display | Generally speaking they are the same. There are a few tiny differences between these two terms which will be explained in this section later on. |
HUD | Heads-up-Display. A type of display for displaying information that does not interfere with the player's controls. |
UIEH | User Interface Event Handler. Detects changes to the UI. Explained in this section. |
Config
You will need files for the following parts:
- A config
- Basic control classes
- A display config
Your folder structure could look something like this:
mission.World/
├── mission.sqm
├── description.ext
├── UI/
│ ├── BaseControls.hpp
│ ├── RscDisplayName.hpp
If you are making a mod the description.ext will be called config.cpp and there is no mission.sqm. We will call the description.ext and config.cpp the main config to cover both.
Main Config Content
All display classes are defined in here. Since the config can get very long we will instead include the files in one another with the #include
preprocessor:
#include "UI\BaseControls.hpp"
#include "UI\RscDisplayName.hpp"
Parent Controls
Also known as base controls. They are the controls that we will be inheriting from. This means that we will copy the content of the parent class without having to rewrite every class. Each parent class has its own unique functionality or appearance determined by their attributes, for example the color of the background is determined by the colorBackground
attribute. If we inherit from this parent class then our dialog control will have the same background color as the parent class. The concept of class inheritance is explained here. There are three ways to declare these base classes.
Import Classes Via import Keyword (Mission Only)
2.02 You can use the base classes from the game config by using the import keyword:
import RscObject;
import RscText;
import RscFrame;
import RscLine;
import RscProgress;
import RscPicture;
import RscPictureKeepAspect;
import RscVideo;
import RscHTML;
import RscButton;
import RscShortcutButton;
import RscEdit;
import RscCombo;
import RscListBox;
import RscListNBox;
import RscXListBox;
import RscTree;
import RscSlider;
import RscXSliderH;
import RscActiveText;
import RscActivePicture;
import RscActivePictureKeepAspect;
import RscStructuredText;
import RscToolbox;
import RscControlsGroup;
import RscControlsGroupNoScrollbars;
import RscControlsGroupNoHScrollbars;
import RscControlsGroupNoVScrollbars;
import RscButtonTextOnly;
import RscButtonMenu;
import RscButtonMenuOK;
import RscButtonMenuCancel;
import RscButtonMenuSteam;
import RscMapControl;
import RscMapControlEmpty;
import RscCheckBox;
Declare Classes (Addon Only)
We have access to the classes from the game's config when we declare them beforehand.
class RscObject;
class RscText;
class RscFrame;
class RscLine;
class RscProgress;
class RscPicture;
class RscPictureKeepAspect;
class RscVideo;
class RscHTML;
class RscButton;
class RscShortcutButton;
class RscEdit;
class RscCombo;
class RscListBox;
class RscListNBox;
class RscXListBox;
class RscTree;
class RscSlider;
class RscXSliderH;
class RscActiveText;
class RscActivePicture;
class RscActivePictureKeepAspect;
class RscStructuredText;
class RscToolbox;
class RscControlsGroup;
class RscControlsGroupNoScrollbars;
class RscControlsGroupNoHScrollbars;
class RscControlsGroupNoVScrollbars;
class RscButtonTextOnly;
class RscButtonMenu;
class RscButtonMenuOK;
class RscButtonMenuCancel;
class RscButtonMenuSteam;
class RscMapControl;
class RscMapControlEmpty;
class RscCheckBox;
Export Classes Via BIS_fnc_exportGUIBaseClasses
Run this command from the debug console:
["Default"] call BIS_fnc_exportGUIBaseClasses;
The result is copied to the clipboard. Paste it into BaseControls.hpp.
Display Config
A display class looks like this:
class RscDisplayName
{
idd = 1234;
class ControlsBackground
{
};
class Controls
{
};
};
RscDisplayName
is the name of the display which will be used in the createDisplay/createDialog commands.
idd
is the identification number for the display. It is used in the findDisplay command. It is mandatory to have it defined. If you don't intend to use the idd you can set it to -1.
ControlsBackground
contains all controls that should stay in the background, for example the dark background of the display.
Controls
contains all important controls, for example buttons.
Controls Config
The most common way to create a UI in Arma 3 is via the Arma 3: User Interface Editor. The BIKI page contains a tutorial on it too. You might also be interested in some of the external UI editors listed here.
A possible output from the GUI Editor might look like this:
class RscButton_1600: RscButton
{
idc = 1600;
x = GUI_GRID_CENTER_X + 0 * GUI_GRID_CENTER_W;
y = GUI_GRID_CENTER_Y + 0 * GUI_GRID_CENTER_H;
w = 40 * GUI_GRID_CENTER_W;
h = 25 * GUI_GRID_CENTER_H;
};
The idc
is the identification number for a control. It is used in the displayCtrl command and can be returned by the ctrlIDC command.
x
and y
determine the position of the control. w
and h
determine the size. These numbers are given in screen coordinates. They are somewhat complicated so read about them on the linked page. In the example the GUI_GRID_CENTER_X/Y/W/H
macro is used to keep the UI in the middle of the screen on all possible screen resolutions and UI sizes.
Apart from the editable attributes in the GUI Editor there are even more. Which exactly depends on the type
of the control. Here is an overview over all available control types (CTs).
Control Types / MACRO (TYPE VALUE) | |
---|---|
Text/Image/Video |
CT_STATIC (0) | CT_EDIT (2) | CT_HTML (9) | CT_STRUCTURED_TEXT (13) |
Buttons |
CT_BUTTON (1) | CT_ACTIVETEXT (11) | CT_SHORTCUTBUTTON (16) | CT_CHECKBOX (77) | CT_XBUTTON (41) |
Lists |
CT_COMBO (4) | CT_TOOLBOX (6) | CT_CHECKBOXES (7) | CT_TREE (12) | CT_CONTROLS_TABLE (19) | CT_XCOMBO (44) | CT_LISTBOX (5) | CT_LISTNBOX (102) | CT_LISTNBOX_CHECKABLE (104) | CT_XLISTBOX (45) |
3D Objects |
CT_OBJECT (80) | CT_OBJECT_ZOOM (81) | CT_OBJECT_CONTAINER (82) | CT_OBJECT_CONT_ANIM (83) |
Maps |
CT_MAP (100) | CT_MAP_MAIN (101) |
Meta |
CT_SLIDER (3) | CT_XSLIDER (43) | CT_PROGRESS (8) | CT_CONTROLS_GROUP (15) | CT_WEBBROWSER (106) | CT_EXTENSION (107) |
Menu |
CT_CONTEXT_MENU (14) | CT_MENU (46) | CT_MENU_STRIP (47) |
Unknown |
CT_STATIC_SKEW (10) | CT_HITZONES (17) | CT_VEHICLETOGGLES (18) | CT_XKEYDESC (40) | CT_ANIMATED_TEXTURE (45) | CT_LINEBREAK (98) | CT_USER (99) | CT_ITEMSLOT (103) | CT_VEHICLE_DIRECTION (105) |
HUDs
A Head-Up-Display is just another type of display in Arma 3. All of the above applies to them too. The differences are:
- The player will be able to move and look around while the display is open but can not interact with it.
- The display class has to be listed as part of the RscTitles class:
#include "UI\BaseControls.hpp"
class RscTitles
{
#include "UI\RscMyHUD.hpp"
};
- The display class needs the
duration
attribute. It determines how long the display will stay on screen. You can choose a large number to make it stay "forever", for example 10^6 (scientific notation: 1e+6) seconds.
class RscMyHUD
{
idd = -1;
duration = 1e+6;
{...
- The commands for controlling the display are also different:
- HUDs are created with cutRsc.
- findDisplay does not work on RscTitles displays. Save the display as a variable to uiNamespace instead. You can get the display with the following code:
uiNamespace getVariable ["RscMyHUD", displayNull];
class RscMyHUD
{
idd = -1;
onLoad = "uiNamespace setVariable ['RscMyHUD', _this select 0];";
duration = 1e+6;
class Controls
{...
Scripting
To bring your dialog to life you will need to know how to influence it with sqf commands. A list of all available UI related commands can be found here. A list of GUI related functions can be found here. Some control types have special commands such as lbAdd to add an item to a listbox. A list of commands that are related to the control type can be found on the control type's BIKI page.
createDialog vs createDisplay vs cutRsc
createDialog, createDisplay and cutRsc (for HUDs) have all their own unique use cases. Here is an overview over what each command does or does not do:
createDialog | createDisplay | cutRsc | |
---|---|---|---|
Interactable | |||
Player can move | Depends on the parent display. | ||
Player can look around | |||
Escape closes display | |||
Can be returned by findDisplay | |||
Returns created display | |||
Can be created on top of another display | (not recommended) | (preferred method) | (will coexist with other displays but is still not interactable with) |
User Interface Event Handlers
User interface event handlers (UIEH) are a way to detect changes to the UI. A list of them can be found here. Once again, different control types have different UIEHs. For example onButtonClick will detect when a button is clicked. The arguments that are passed to the script also depend on the UIEH. The onButtonClick event will pass the button itself as the only argument in the _this variable. On the other hand onLBSelChanged will pass the control and the selected index as arguments into the script. Since the UIEH is a different script instance, all previously defined local variables will not be available in the code. There are two ways to add an UIEH to a control:
Adding UIEHs via config
The UIEH is given as an attribute of the control's class like this:
class ClickMe: RscButton
{
idc = -1;
text = "Click Me!";
onButtonClick = "hint 'You clicked the button!';"; // Display a hint when clicked upon
x = GUI_GRID_CENTER_X + 10 * GUI_GRID_CENTER_W;
y = GUI_GRID_CENTER_Y + 12 * GUI_GRID_CENTER_H;
w = 20 * GUI_GRID_CENTER_W;
h = 1 * GUI_GRID_CENTER_H;
};
The UIEH's name always starts with "on". The code that should be executed is given as a string.
Adding UIEHs via script
To add UIEHs you can also use ctrlAddEventHandler. In this case the UIEH does NOT start with "on"!
// This script does the same as the config example
_display = findDisplay 1234;
_ctrl = _display displayCtrl 1000;
_ctrl ctrlAddEventHandler ["ButtonClick", { // Notice the missing "on"!
params ["_ctrl"];
hint "You clicked the button!";
}];
UI Variables and Serialization
Variables containing Displays or Controls are not serializable, meaning they can not be stored in save files such as those used by saveGame and loadGame.
While treating UI variables like other variables does not crash the game when saving, it is at least bad practice and leads to error log entries (see Crash Files).
UI variables should therefore be used properly like so:
Use the uiNamespace to store UI variables outside of scripts and functions.
// Wrong, stores MissionDisplay in the missionNamespace: MissionDisplay = findDisplay 46;
// Correct: with uiNamespace do { MissionDisplay = findDisplay 46; };
// Also correct: uiNamespace setVariable ["MissionDisplay", findDisplay 46];
Use disableSerialization before introducing UI variables in scripts and functions.
// Wrong: params [["_myCtrl", controlNull, [controlNull]], ["_text", "", [""]]]; _myCtrl ctrlSetText _text;
// Correct: disableSerialization; params [["_myCtrl", controlNull, [controlNull]], ["_text", "", [""]]]; _myCtrl ctrlSetText _text;
Final Result
description.ext or config.cpp
#include "\a3\ui_f\hpp\defineCommonGrids.inc"
#include "UI\BaseControls.hpp"
#include "UI\RscDisplayMyDialog.hpp"
class RscTitles
{
#include "UI\RscMyHUD.hpp"
};
BaseControls.hpp
It is only necessary to import/declare the base classes that you actually intend to use. More usable base controls can be found in the base config of the game. Use the ingame Config Viewer to find them.
Mission
import RscObject;
import RscText;
import RscFrame;
import RscLine;
import RscProgress;
import RscPicture;
import RscPictureKeepAspect;
import RscVideo;
import RscHTML;
import RscButton;
import RscShortcutButton;
import RscEdit;
import RscCombo;
import RscListBox;
import RscListNBox;
import RscXListBox;
import RscTree;
import RscSlider;
import RscXSliderH;
import RscActiveText;
import RscActivePicture;
import RscActivePictureKeepAspect;
import RscStructuredText;
import RscToolbox;
import RscControlsGroup;
import RscControlsGroupNoScrollbars;
import RscControlsGroupNoHScrollbars;
import RscControlsGroupNoVScrollbars;
import RscButtonTextOnly;
import RscButtonMenu;
import RscButtonMenuOK;
import RscButtonMenuCancel;
import RscButtonMenuSteam;
import RscMapControl;
import RscMapControlEmpty;
import RscCheckBox;
Addon
class RscObject;
class RscText;
class RscFrame;
class RscLine;
class RscProgress;
class RscPicture;
class RscPictureKeepAspect;
class RscVideo;
class RscHTML;
class RscButton;
class RscShortcutButton;
class RscEdit;
class RscCombo;
class RscListBox;
class RscListNBox;
class RscXListBox;
class RscTree;
class RscSlider;
class RscXSliderH;
class RscActiveText;
class RscActivePicture;
class RscActivePictureKeepAspect;
class RscStructuredText;
class RscToolbox;
class RscControlsGroup;
class RscControlsGroupNoScrollbars;
class RscControlsGroupNoHScrollbars;
class RscControlsGroupNoVScrollbars;
class RscButtonTextOnly;
class RscButtonMenu;
class RscButtonMenuOK;
class RscButtonMenuCancel;
class RscButtonMenuSteam;
class RscMapControl;
class RscMapControlEmpty;
class RscCheckBox;
RscDisplayMyDialog.hpp
class RscDisplayMyDialog
{
idd = 1234;
class ControlsBackground
{
class Background: RscText
{
idc = -1;
x = GUI_GRID_CENTER_X;
y = GUI_GRID_CENTER_Y;
w = 40 * GUI_GRID_CENTER_W;
h = 25 * GUI_GRID_CENTER_H;
colorBackground[] = {0,0,0,0.8};
};
};
class Controls
{
class ClickMe: RscButton
{
idc = -1;
text = "Click Me!";
onButtonClick = "hint 'You clicked the button!';";
x = GUI_GRID_CENTER_X + 10 * GUI_GRID_CENTER_W;
y = GUI_GRID_CENTER_Y + 12 * GUI_GRID_CENTER_H;
w = 20 * GUI_GRID_CENTER_W;
h = 1 * GUI_GRID_CENTER_H;
};
};
};
RscMyHUD.hpp
class RscMyHUD
{
idd = -1;
onLoad = "uiNamespace setVariable ['RscMyHUD', _this select 0];";
duration = 10;
fadeIn = 1;
fadeOut = 1;
class Controls
{
class CenterText: RscStructuredText
{
text = "This text box will stay here for 10 seconds. You can still move and look around.";
x = GUI_GRID_CENTER_X;
y = GUI_GRID_CENTER_Y;
w = 40 * GUI_GRID_CENTER_W;
h = 25 * GUI_GRID_CENTER_H;
colorBackground[] = {0,0,0,0.8};
};
};
};
Summary
- A UI consists of the following parts:
- Base controls to inherit from
- The display
- UIEHs
- You can get the base controls in a few different ways
- The display contains a list of (non) interactable controls
- These controls can have different styles and functionalities
- You can use the GUI Editor or external tools to have a "What You See Is What You Get" approach
- UIEHs can detect interactions with the UI
Afterword
Now it is up to you to create some UIs. If you have questions feel free to ask them on the BI Forums for mission makers or addon makers. You can also find a Discord channel dedicated to GUI editing on the Arma 3 Discord.
Advanced UI Creation
This part will list some of the more advanced techniques to create and handle UIs. The list is somewhat unordered, as it is more of a list of "nice to know" things.
Faster Debugging
Mission
The mission config is reloaded every time the mission is saved or when you return from the preview to Eden. Instead of previewing the mission and creating your UI it is possible to instead preview the UI in Eden directly. Execute the following command in the Debug Console while in Eden:
findDisplay 313 createDisplay "RscDisplayAAR";
Your UI is created on top of the Eden display (IDD: 313). Now you can simply make changes to the UI, close the display, save the mission and execute the command again. The changes should now take effect.
Addon
The diag_mergeConfigFile command will enable you to reload the UI's config without having to restart the game or repack the mod. Here is a little script that would do just that:
diag_mergeConfigFile ["P:\MyModFolder\config.cpp"]; ([findDisplay 49, findDisplay 313] select is3DEN) createDisplay "RscDisplayAAR";
The second line either creates your dialog on top of the Eden display, if you are in Eden, or on top of the escape menu when you are ingame.
BIS_fnc_initDisplay
When creating a mod you are able to utilize BIS_fnc_initDisplay which will handle parts of your UI. As an example we will be taking a look at an Arma 3 display called RscDisplayAAR
.
Compiling display script to uiNamespace
The partial config of RscDisplayAAR
looks like this:
class RscDisplayAAR
{
scriptName = "RscDisplayAAR";
scriptPath = "GUI";
onLoad = "[""onLoad"",_this,""RscDisplayAAR"",'GUI'] call (uinamespace getvariable 'BIS_fnc_initDisplay')";
onUnload = "[""onUnload"",_this,""RscDisplayAAR"",'GUI'] call (uinamespace getvariable 'BIS_fnc_initDisplay')";
idd = 2121;
//...
We can utilize the INIT_DISPLAY
macro from "\a3\ui_f\hpp\defineCommon.inc" to shorten that config:
#include "\a3\ui_f\hpp\defineCommon.inc"
class RscDisplayAAR
{
INIT_DISPLAY(RscDisplayAAR,GUI)
idd = 2121;
//...
Now let's see what these attributes do. On game start BIS_fnc_initDisplay will look through the following configs to search for UIs:
- configFile
- configFile >> "RscTitles"
- configFile >> "RscIngameUI"
- configFile >> "Cfg3den" >> "Attributes"
If a class has the attributes scriptName
and scriptPath
(and the attribute scriptIsInternal
is not defined or 0) then the display function is compiled into uiNamespace in the following way:
scriptPath
points to a config attribute inconfigFile >> "CfgScriptPaths"
- The value of that attribute points to a folder which contains the sqf file with the name provided by
scriptName
- This script is compiled to uiNamespace as the value given by the
scriptName
attribute and appended by "_script"
In case of RscDisplayAAR
:
scriptPath
is "GUI"- The value of the attribute "GUI" from
configFile >> "CfgScriptPaths"
is "A3\ui_f\scripts\GUI\" - The script "A3\ui_f\scripts\GUI\RscDisplayAAR.sqf" is compiled as
RscDisplayAAR_script
to uiNamespace
Handling onLoad and onUnload UIEHs
BIS_fnc_initDisplay is meant to be called from the onLoad and onUnload UIEH of the display as you can see in the config above. In both UIEHs the display's function is called with the following parameters:
params ["_mode", "_params", "_class"];
- _mode: String - either "onLoad" or "onUnload"
- _params: Array - The parameters of the onLoad or onUnload UIEH
- _class: String - The classname of the display
Here is an overview of the variables that are introduced by BIS_fnc_initDisplay. All variables are updated in the onLoad and onUnload UIEH.
Variable | Namespace | Explanation | Example |
---|---|---|---|
RscDisplayName_script | uiNamespace | The display's script as defined by the scriptPath and scriptName attributes |
_script = uiNamespace getVariable "RscDisplayAAR_script"
|
RscDisplayName | uiNamespace | Reference to the display which can be used with GUI commands | _display = uiNamespace getVariable "RscDisplayAAR"
|
BIS_fnc_initDisplay_configClass | Display | Config path of the display | _configName = _display getVariable "BIS_fnc_initDisplay_configClass"
|
PREFIX_displays | uiNamespace | List of open displays with the PREFIX provided as the fourth param of the function | _displays = uiNamespace getVariable "GUI_displays";
|
Scripted Event Handlers
The function calls the following Scripted Eventhandlers:
OnDisplayRegistered
OnDisplayUnregistered
In both cases the scripted eventhandlers are executed in uiNamespace and the display and the classname of the display are passed as arguments.
params ["_display", "_class"];
Example:
[] spawn { [uiNamespace, "OnDisplayRegistered", { params ["_display", "_class"]; if (_class == "RscDisplayFunctionsViewer") then { systemChat "You opened the Functions Viewer!"; }; }] call BIS_fnc_addScriptedEventHandler; [uiNamespace, "OnDisplayUnregistered", { params ["_display", "_class"]; if (_class == "RscDisplayFunctionsViewer") then { systemChat "You closed the Functions Viewer!"; }; }] call BIS_fnc_addScriptedEventHandler; // Execute in Eden: _display = findDisplay 313 createDisplay "RscDisplayFunctionsViewer"; // -> You opened the Functions Viewer! uiSleep 2; _display closeDisplay 1; // -> You closed the Functions Viewer! };