User Interface
In UNIGINE a Graphical User Interface (GUI) is composed of different types of widgets added to it. Basically, there are two ways of creating GUI:
- By adding widgets to the system GUI (UNIGINE user interface) that is rendered on top of application window. In this case we use the Gui class.
- By adding widgets to a GUI object positioned in the world. In this case, any postprocessing filter can be applied. By creating a specific Gui object (or GuiMesh for arbitrary geometry) that can be placed anywhere in the scene, and adding widgets to it. This can be useful, for example, to implement interaction with the interface displayed on a computer screen in a room (i.e. UI bound to an object that can be viewed from different angles). In this case, a post-processing filter can be applied.
There are 2 ways to create the GUI layout:
- Directly from code via GUI classes
- Using UI files with the description of user interface. Such a description file in XML-like format can be created for a complex interface to write less code.
The following code demonstrates how to add Label and Slider widgets to the system GUI:
// getting a reference to the system GUI
gui = Gui::getCurrent();
// creating a label widget and setting up its parameters
WidgetLabelPtr widget_label = WidgetLabel::create(gui, "Label text:");
widget_label->setToolTip("This is my label!");
widget_label->arrange();
widget_label->setPosition(10, 10);
// creating a slider widget and setting up its parameters
WidgetSliderPtr widget_slider = WidgetSlider::create(gui, 0, 360, 90);
widget_slider->setToolTip("This is my slider!");
widget_slider->arrange();
widget_slider->setPosition(100, 10);
// adding widgets to the system GUI
gui->addChild(widget_label, Gui::ALIGN_OVERLAP | Gui::ALIGN_FIXED);
gui->addChild(widget_slider, Gui::ALIGN_OVERLAP | Gui::ALIGN_FIXED);
In order to use GUI elements (not just watch them rendered) we must specify handlers for various events (click, change, etc.). The following code demonstrates how to set event handlers:
// EventConnections class instance to manage event subscriptions
EventConnections econnections;
/// function to be called when button1 is clicked
int onButton1Clicked(const WidgetPtr &button)
{
/* .. */
}
/// method to be called when slider position is changed
int AppWorldLogic::onSliderChanged(const WidgetPtr &slider)
{
/* .. */
}
int AppWorldLogic::init()
{
// getting the reference to the system GUI
GuiPtr gui = Gui::getCurrent();
/* .. */
// adding the widget to the system GUI
gui->addChild(widget_slider, Gui::ALIGN_OVERLAP | Gui::ALIGN_FIXED);
// subscbribing for the clicked event with the onButton1Clicked function as a handler
buttonwidget1->getEventClicked().connect(econnections, onButton1Clicked);
// setting AppWorldLogic::onSliderChanged method as a changed event handler for a widget_slider
widget_slider->getEventChanged().connect(econnections, this, &AppWorldLogic::onSliderChanged);
/* .. */
return 1;
}
int AppWorldLogic::shutdown()
{
// removing all event subscriptions somewhere on shutdown
econnections.disconnectAll();
return 1;
}
Practice#
In architectural visualization projects, a widespread option is changing materials on various objects, for example, to preview different wallpapers on the walls and match them with the decor of the furniture. For our project, let's create a component (also inherit it from Interactable) that displays a list of available materials for a selected object in the UI based on ObjectGui with the ability to select and automatically apply them.
-
Create a new component called MaterialCustomizer and add the following code to it:
#pragma once #include <UnigineComponentSystem.h> #include "Interactable.h" class MaterialCustomizer : public Interactable { public: // declare constructor and destructor for our class and define the name of the property to be associated with the component. // The MaterialCustomizer.prop file containing all parameters listed below will be generated in your project's data folder after running the app for the first time COMPONENT_DEFINE(MaterialCustomizer, Interactable); // List of materials for object customization PROP_ARRAY(Material, MaterialsList); // registering methods to be called at the corresponding stages of the world logic (methods are declared in the protected-section below) COMPONENT_INIT(init); // Action method override for the child-component implementing the context menu void action(int num = 0); protected: // declaration of methods to be called at the corresponding stages of the world logic void init(); private: // GUI object for displaying of the material selection interface Unigine::ObjectGuiPtr gui = nullptr; // the function that assigns the material selected in the menu to the selected customized object's surface void select_material_handler(const Unigine::WidgetPtr& w); // EventConnections class instance to manage event subscriptions Unigine::EventConnections econn; };
#include "MaterialCustomizer.h" #include "InputProcessor.h" REGISTER_COMPONENT(MaterialCustomizer); using namespace Unigine; using namespace Math; // the function that assigns the material selected in the menu to the selected customized object's surface void MaterialCustomizer::select_material_handler(const WidgetPtr& w) { Unigine::ObjectPtr object = checked_ptr_cast<Unigine::Object>(node); Unigine::WidgetComboBoxPtr combo_box = checked_ptr_cast<Unigine::WidgetComboBox>(w); object->setMaterial(MaterialsList[combo_box->getCurrentItem()], ComponentSystem::get()->getComponentInWorld<InputProcessor>()->intersection->getSurface()); } void MaterialCustomizer::init() { // setting the text of the tooltip that will be displayed when the cursor hovers over the object tooltip = "Right-click displays a menu where you can select a material for the object."; // creating the GUI object to display the material selection interface and setting its size and resolution gui = ObjectGui::create(0.5f, 0.5f); gui->setScreenSize(600, 600); // enabling intersection detection to interact with UI using the mouse gui->setIntersection(true, 0); // making the interface always rotate to the user and not blocked by other objects gui->setBillboard(true); gui->getMaterialInherit(0)->setDepthTest(false); // disabling the black background and temporarily hiding the UI gui->setBackground(false); gui->setEnabled(false); // setting the distance from which interaction with the interface is possible gui->setControlDistance(10); if (MaterialsList.size() > 0) { // adding the WidgetVBox widget container for vertical layout of elements, setting indents WidgetVBoxPtr vbox = WidgetVBox::create(gui->getGui()); vbox->setSpace(20, 20); // enabling the background and setting its color vbox->setBackground(1); vbox->setBackgroundColor(vec4(0.1f, 0.5f, 0.1f, 0.4f)); // creating a WidgetLabel widget to display the header, setting its position, and font size WidgetLabelPtr label = WidgetLabel::create(gui->getGui(), "Select Material:"); label->setFontSize(50); // adding the Label widget to the container vbox->addChild(label, Gui::ALIGN_TOP); // creating a WidgetComboBox widget to display a drop-down list of materials, and setting its font size WidgetComboBoxPtr mat_menu = WidgetComboBox::create(gui->getGui()); mat_menu->setFontSize(40); // setting the handler on selection of an item from the list mat_menu->getEventChanged().connect(econn, this, &MaterialCustomizer::select_material_handler); // adding items to the menu according to the list of materials for (int i = 0; i < MaterialsList.size(); i++) { Unigine::String str = FileSystem::getVirtualPath(MaterialsList[i].get()->getFilePath()); mat_menu->addItem(str.substr(str.rfind("/") + 1)); } // adding the ComboBox widget to the container vbox->addChild(mat_menu, Gui::ALIGN_TOP); // adding container widget to GUI gui->getGui()->addChild(vbox, Gui::ALIGN_EXPAND | Gui::ALIGN_OVERLAP); } } // Action method override for the child-component void MaterialCustomizer::action(int num) { // action indices different from zero are invalid for this component, so they are ignored if (num != 0) return; // placing the GUI near the click point gui->setWorldPosition(ComponentSystem::get()->getComponentInWorld<InputProcessor>()->intersection->getPoint()); // hide the GUI for all other customizable objects (if any) Unigine::Vector<MaterialCustomizer*> customizer_components; ComponentSystem::get()->getComponentsInWorld(customizer_components); for (MaterialCustomizer* customizer : customizer_components) if (this != customizer) customizer->gui->setEnabled(false); // show or hide the interface of material selection for the object gui->setEnabled(!gui->isEnabled()); }
Let's save our files and then build and run our application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign our component to nodes. Close the application after running it and switch to UnigineEditor.
- Assign our new property to the bedside_table_1 object in our scene.
-
Fill in the list of materials. To do this, change the number of items in the list from 0 to 3 and then drag three materials into the corresponding fields.
Upon right-clicking an object a list of available materials will be displayed, you can choose the desired one, and it will be applied automatically right as you click it: