This page has been translated automatically.
Video Tutorials
Interface
Essentials
Advanced
How To
Rendering
Professional (SIM)
UnigineEditor
Interface Overview
Assets Workflow
Version Control
Settings and Preferences
Working With Projects
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Lighting
Sandworm
Using Editor Tools for Specific Tasks
Extending Editor Functionality
Built-in Node Types
Nodes
Objects
Effects
Decals
Light Sources
Geodetics
World Nodes
Sound Objects
Pathfinding Objects
Players
Programming
Fundamentals
Setting Up Development Environment
Usage Examples
C++
C#
UnigineScript
UUSL (Unified UNIGINE Shader Language)
Plugins
File Formats
Materials and Shaders
Rebuilding the Engine Tools
GUI
Double Precision Coordinates
API
Animations-Related Classes
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
VR-Related Classes
Content Creation
Content Optimization
Materials
Material Nodes Library
Miscellaneous
Input
Math
Matrix
Textures
Art Samples
Tutorials

Creating End UI

The end widget should be visible when the game is over. The user will be able to restart the game or exit the application via the corresponding buttons.

Step 1. Make the World Widget Controller#

The widget controller node will specify the handlers for different types of UI elements and show the UI at the end of the game.

  1. Create a new C++ component and call it UiElement. This component handles the selection and button functions calls. Write the following code and save your code in your IDE.

    UiElement.h (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include <UnigineGame.h>
    
    class UiElement : public Unigine::ComponentBase
    {
    public:
    	// declare constructor and destructor for our class and define a property name. 
    	COMPONENT_DEFINE(UiElement, ComponentBase)
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	enum Element { Restart, Exit, None };
    
    	// default type of a UI element
    	Element uiType = Element::None;
    
    	// scale on cursor hover
    	float selectScale = 1;
    
    	// emission color on cursor hover
    	Unigine::Math::vec4 selectEmission = Unigine::Math::vec4_black;
    
    	// default mask for the mouse intersection detection
    	int uiMask = 1;
    
    	static void setOnClickCallback(Unigine::CallbackBase *callback) { onClickCallback = callback; }
    
    protected:
    	void init();
    	void update();
    
    	void onWindowResize();
    	void onDisable();
    	void onLeave();
    	void onEnter();
    
    private:
    	static Unigine::CallbackBase *onClickCallback;
    
    	// ID of the UI element
    	int Id; 
    
    	// ID counter for initialization  
    	static int idCount;
    
    	// counter of selected objects
    	int selectedObjects = 0;
    
    	Unigine::WorldIntersectionPtr intersection;
    
    	bool isSelect = false;
    
    	Unigine::ObjectPtr uiObject = nullptr;
    
    	Unigine::Math::vec3 sourceScale = Unigine::Math::vec3_one;
    	Unigine::Math::vec4 sourceEmission = Unigine::Math::vec4_one;
    };
    UiElement.cpp (C++)
    #include "UiElement.h"
    
    using namespace Unigine;
    using namespace Math;
    
    REGISTER_COMPONENT(UiElement);
    
    CallbackBase *UiElement::onClickCallback = nullptr;
    
    int UiElement::idCount = 0;
    
    void UiElement::init()
    {
    	// set ID
    	Id = idCount;
    	++idCount;
    
    	selectedObjects = 0;
    
    	// get the UI element
    	uiObject = checked_ptr_cast<Object>(node);
    
    	// remember the source scale and emission color
    	sourceScale = node->getScale();
    	if (uiObject)
    		sourceEmission = uiObject->getMaterialParameterFloat4("emission_color", 0);
    
    	intersection = WorldIntersection::create();
    }
    
    void UiElement::update()
    {
    	// taking into account main window position to obtain correct direction
    	Math::ivec2 mouse = Input::getMousePosition();
    
    	// get points for intersection
    	vec3 dir = Game::getPlayer()->getDirectionFromMainWindow(mouse.x, mouse.y);
    	Vec3 p0 = Game::getPlayer()->getWorldPosition();
    	Vec3 p1 = p0 + Vec3(dir * 25.0f);
    
    	// find the intersection
    	ObjectPtr obj = World::getIntersection(p1, p0, uiMask, intersection);
    	if (obj)
    	{
    		// try to get the UI element component and select/deselect it
    		UiElement *uiElement = ComponentSystem::get()->getComponent<UiElement>(obj);
    		if (uiElement && uiElement->Id == Id)
    		{
    			if (!isSelect)
    			{
    				UiElement::onEnter();
    				isSelect = true;
    				++selectedObjects;
    			}
    		}
    		else if (isSelect)
    		{
    			UiElement::onLeave();
    			isSelect = false;
    			--selectedObjects;
    		}
    	}
    	else
    	{
    		if (isSelect)
    		{
    			UiElement::onLeave();
    			isSelect = false;
    			--selectedObjects;
    		}
    	}
    
    	// run the mouse click callback
    	if (isSelect && Input::isMouseButtonDown(Input::MOUSE_BUTTON::MOUSE_BUTTON_LEFT) && onClickCallback)
    		onClickCallback->run(uiType);
    }
    
    void UiElement::onDisable()
    {
    	// deselect an object
    	if (isSelect)
    	{
    		--selectedObjects;
    		if (selectedObjects < 0)
    			selectedObjects = 0;
    
    		isSelect = false;
    		UiElement::onLeave();
    	}
    }
    
    void UiElement::onEnter()
    {
    	// set the visual effect on selection
    	node->setScale(sourceScale * selectScale);
    	uiObject->setMaterialParameterFloat4("emission_color", selectEmission, 0);
    }
    
    void UiElement::onLeave()
    {
    	// remove the visual effect when the UI element is not selected anymore
    	node->setScale(sourceScale);
    	uiObject->setMaterialParameterFloat4("emission_color", sourceEmission, 0);
    }
  2. Create a new C++ component and call it EndWidget. Copy the code below and paste it to the corresponding files in your project and save them in your IDE.

    EndWidget.h (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include "LevelManager.h"
    #include "UiElement.h"
    #include <UnigineConsole.h>
    
    class EndWidget : public Unigine::ComponentBase
    {
    public:
    	// declare constructor and destructor for our class and define a property name. 
    	COMPONENT_DEFINE(EndWidget, ComponentBase)
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	
    	// object with the end game message
    	PROP_PARAM(Node, endGameWidget);
    
    protected:
    	void init();
    
    	void endGameEventHandler();
    	void onClickHandler(UiElement::Element uiType);
    
    private:
    	LevelManager *levelManager;
    };
    EndWidget.cpp (C++)
    #include "EndWidget.h"
    
    using namespace Unigine;
    
    REGISTER_COMPONENT(EndWidget);
    
    void EndWidget::init()
    {
    	levelManager = ComponentSystem::get()->getComponent<LevelManager>(World::getNodeByName("level_manager"));
    
    	if (levelManager)
    		levelManager->setEndGameCallback(MakeCallback(this, &EndWidget::endGameEventHandler));
    
    	// set the mouse click handler for UI elements (Restart/Exit)
    	UiElement::setOnClickCallback(MakeCallback(this, &EndWidget::onClickHandler));
    
    	if (endGameWidget)
    	{
    		// hide the end UI
    		endGameWidget->setEnabled(false);
    	}
    }
    
    void EndWidget::endGameEventHandler()
    {
    	// set gui and input
    	Input::setMouseHandle(Input::MOUSE_HANDLE::MOUSE_HANDLE_USER);
    
    	// show the end UI
    	endGameWidget->setEnabled(true);
    }
    
    void EndWidget::onClickHandler(UiElement::Element uiType)
    {
    	// restart the level by reloading the world
    	if (uiType == UiElement::Element::Restart)
    	{
    		Unigine::Console::run("world_reload");
    	}
    
    	// exit the game
    	if (uiType == UiElement::Element::Exit)
    		Engine::get()->quit();
    }
  3. Build and run the solution in your IDE to regenerate the properties.
  4. Switch to the UnigineEditor and create a new Dummy Node. Call it "widgets_controller".
  5. Assign the EndWidget property to the widgets_controller node.

Step 2. Set Up the World Widget#

The end widget provides button functionality and additional information about the current game outcome. To properly display the end widget, position it in front of the camera.

  1. Drag the programming_quick_start\ui\ui_plane.node from the Asset Browser to the world.
  2. Parent it to the PlayerDummy camera. Set the position of the ui_plane node to the following values.

  3. Save changes to the world, go to File->Save World or press Ctrl + S hotkey.
  4. Next, we add a win condition check, end widget text, and a callback run to the LevelManager class. Let's also assign the UiElement component to the end widget's buttons right there in the LevelManager class.

    Set the Ui Type to Restart, check the 6th bit option for the Ui Mask (matching the button's Intersection Mask) to correctly process selection intersections and set the Select Scale to 1.05 in order to make buttons bigger on cursor hover. Also, set the Select Emission to #505050 to make the button change its color.

    To do so, open the LevelManager component in your IDE and replace it with the following code. Don't forget to save your code.

    LevelManager.h (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include <UnigineWidgets.h>
    #include <UnigineGame.h>
    #include <UnigineString.h>
    //========================== NEW - BEGIN ===============================
    #include "UiElement.h"
    //=========================== NEW - END ================================
    
    class LevelManager : public Unigine::ComponentBase
    {
    public:
    	// declare constructor and destructor for our class and define a property name. 
    	COMPONENT_DEFINE(LevelManager, ComponentBase)
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init, 2); // 2nd initialization order
    	COMPONENT_UPDATE(update);
    	COMPONENT_SHUTDOWN(shutdown);
    	// level timer
    	PROP_PARAM(Float, timer, 100.0f);
    //========================== NEW - BEGIN ===============================
    	PROP_PARAM(Node, restartButton);
    	PROP_PARAM(Node, exitButton);
    //=========================== NEW - END ================================
    	void decPhysicalObjectsNum();
    	void setEndGameCallback(Unigine::CallbackBase *callback) { endGameEvent = callback; }
    
    protected:
    	void init();
    	void update();
    	void shutdown();
    
    	void initGUI();
    
    private:
    	Unigine::CallbackBase *endGameEvent = nullptr;
    
    	bool isCounting = true;
    
    	int physicalObjectsNum;
    
    	Unigine::WidgetLabelPtr widget_timer, widget_goal;
    
    	Unigine::ObjectTextPtr endText;
    };
    LevelManager.cpp (C++)
    #include "LevelManager.h"
    
    using namespace Unigine;
    using namespace Math;
    
    REGISTER_COMPONENT(LevelManager);
    
    void LevelManager::init()
    {
    	LevelManager::initGUI();
    
    	// find the object text node of the widget
    	endText = checked_ptr_cast<ObjectText>(Game::getPlayer()->findNode("header_text", 1));
    
    	// count dynamic objects in the level
    	physicalObjectsNum = node->getNumChildren();
    //========================== NEW - BEGIN ===============================
    	// set up the restart button as a UI element
    	if (restartButton)
    	{
    		UiElement* restart_component = ComponentSystem::get()->addComponent<UiElement>(restartButton);
    		restart_component->uiType = UiElement::Element::Restart;
    		restart_component->uiMask = 0x00000040; // 6th bit is set
    		restart_component->selectScale = 1.05f;
    	}
    
    	// set up the exit button as a UI element
    	if (exitButton)
    	{
    		UiElement* exit_component = ComponentSystem::get()->addComponent<UiElement>(exitButton);
    		exit_component->uiType = UiElement::Element::Exit;
    		exit_component->uiMask = 0x00000040; // 6th bit is set
    	}
    //=========================== NEW - END ================================
    }
    
    void LevelManager::initGUI()
    {
    	// get a GUI pointer
    	GuiPtr gui = Gui::getCurrent();
    
    	// create a label widget and set up its parameters
    	widget_timer = WidgetLabel::create(gui, "Time Left:");
    	widget_timer->setPosition(10, 10);
    	widget_timer->setFontColor(vec4_red);
    
    	widget_goal = WidgetLabel::create(gui, "Objects Left: ");
    	widget_goal->setPosition(10, 30);
    	widget_goal->setFontColor(vec4_blue);
    
    	// add widgets to the GUI
    	gui->addChild(widget_timer, Gui::ALIGN_OVERLAP);
    	gui->addChild(widget_goal, Gui::ALIGN_OVERLAP);
    }
    
    void LevelManager::update()
    {
    	// decrease the timer
    	if (isCounting)
    	{
    		timer = timer - Game::getIFps();
    		if (timer <= 0)
    		{
    			//set end game text
    			if (endText) endText->setText("Game Over");
    			if (endGameEvent) endGameEvent->run();
    			isCounting = false;
    		}
    	}
    
    	// show the current time and objects left to clear
    	if (isCounting)
    	{
    		widget_timer->setText(String::format("Time Left: %.2f s", timer.get()));
    		widget_goal->setText(String::format("Objects Left: %d", physicalObjectsNum));
    	}
    	//hide the widgets on endgame
    	else
    	{
    		widget_timer->setEnabled(false);
    		widget_goal->setEnabled(false);
    	}
    	
    	//win
    	if (physicalObjectsNum <= 0)
    	{
    		if (endText) endText->setText("Success!");
    		if (endGameEvent) endGameEvent->run();
    		isCounting = false;
    	}
    }
    
    void LevelManager::shutdown()
    {
    	widget_timer.deleteLater();
    	widget_goal.deleteLater();
    }
    
    void LevelManager::decPhysicalObjectsNum()
    {
    	physicalObjectsNum--;
    }
  5. Build and run the solution to regenerate a property file for the LevelManager component.
  6. Switch to the UnigineEditor, select the ui_plane node in the World Nodes window and right-click to choose Unpack To Node Content to link the buttons nodes to the LevelManager component.
  7. Then drag the buttons (restart_button and exit_button) to the corresponding fields of the LevelManager property.

  8. Drag the ui_plane node to the End Game Widget field under the EndWidget component of the widgets_controller node.

  9. Save changes to the world, go to File->Save World or press Ctrl + S hotkey.
  10. Switch to your IDE, then build and run the game in your IDE to see the UI in action.
Last update: 2023-12-20
Build: ()