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
VR Development
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 Custom Components

Warning
The functionality described in this article is not available in the Community SDK edition.
You should upgrade to Sim SDK edition to use it.

You can extend the initial set of components that can be added to entities by adding custom ones. In the article, we will consider 2 cases:

  • Only 1 computer is used for simulation (i.e. no synchronization over network).
  • Several computers synchronized over LAN are used for simulation.

Creating a Simple Custom Component#

When only 1 computer is used for simulation, your custom component won't be different from any other component of a UNIGINE-based application. As an example let us consider adding a water drop component for the Be-200 aircraft available in the IG Template.

Follow the instructions given below to create you own custom component.

  1. Create a new project using the IG Template as described here.

    Create a Project using the IG Template and Component System

  2. Create the following files describing your new component to the C++ project:

    • WaterDropAircraftController.h

      Source code (C++)
      #pragma once
      
      #include <UnigineGame.h>
      #include <plugins/Unigine/IG/UnigineIG.h>
      #include <UnigineComponentSystem.h>
      
      class WaterDropAircraftController final: public Unigine::ComponentBase
      {
      public:
      	COMPONENT(WaterDropAircraftController, Unigine::ComponentBase);
      	COMPONENT_INIT(init);
      	COMPONENT_UPDATE(update);
      	COMPONENT_SHUTDOWN(shutdown);
      	// Specifying the name of the property file and parameters for our component
      	PROP_NAME("WaterDropAircraftController");
      	PROP_PARAM(Toggle, open, false, "Open", "Input parameter for enabling/disabling the effect", "Input");
      	PROP_PARAM(Float, normalize_flow, 1.0f, "Normalize Flow", "Input parameter for the effect power normalization", "Input");
      	PROP_PARAM(Float, normalize_payload, 1.0f, "Normalize Payload", "Input parameter for payload normalization", "Input");
      
      	PROP_PARAM(Node, particles_system, "Particles System", "ObjectParticles with the effect", "Effect");
      	PROP_PARAM(Float, spawn_rate_factor, 100.0f, "Spawn Rate Factor", "Multiplier for spawn rate", "Effect");
      
      	PROP_PARAM(Toggle, controlled_payload_time, false, "Controlled Payload Time", "Enable: water effect can be stopped automatically", "Payload")
      	PROP_PARAM(Float, max_water_payload, 100.0f, "Max Water Payload", "Full water payload in units (weight or volume)", "Payload")
      	PROP_PARAM(Float, max_flow_speed, 1.0f, "Max Flow Speed", "Maximum water flow speed (units per second)", "Payload");
      private:
      	void init();
      	void update();
      	void shutdown();
      
      	// Declaring a callback on changing the property parameters
      	void parameterChanged(Unigine::PropertyPtr property, int propID);
      
      	void openWaterDropSystem(bool open);
      	void setWaterDropSystemFlow(float value);
      	void setWaterPayload(float value);
      
      private:
      	// Declaring a particle system to be used for the water drop effect
      	Unigine::ObjectParticlesPtr dropWaterEffect;
      
      	float current_payload = 0.0f;
      	Unigine::Plugins::IG::Manager *ig = nullptr;
      };
    • WaterDropAircraftController.cpp

      Source code (C++)
      #include "WaterDropAircraftController.h"
      #include <UnigineProperties.h>
      #include <UnigineObjects.h>
      #include <UnigineEditor.h>
      // Registering the component in the Component System
      REGISTER_COMPONENT(WaterDropAircraftController);
      using namespace Unigine;
      
      void WaterDropAircraftController::init()
      {
      
      	// Adding a callback on changing the property parameters
      	getProperty()->addCallback(Property::CALLBACK_PARAMETER_CHANGED,
      		MakeCallback(this, &WaterDropAircraftController::parameterChanged));
      	// Creating a particle system for our effect and setting its parameters
      	dropWaterEffect = checked_ptr_cast<ObjectParticles>(particles_system.get());
      	if (!dropWaterEffect)
      		Log::error("WaterDropAircraftController::init(): particles_system node is not ObjectParticles!\n");
      	else
      		dropWaterEffect->setEmitterEnabled(false);
      
      	ig = Plugins::IG::Manager::get();
      }
      void WaterDropAircraftController::update()
      {
      	if (! dropWaterEffect || controlled_payload_time == 0 || open == 0 || normalize_flow <= 0 || current_payload <= 0)
      		return;
      
      	if (!dropWaterEffect->isEmitterEnabled())
      		dropWaterEffect->setEmitterEnabled(true);
      
      	// decrease current payload
      	current_payload -= ig->getIFps() * normalize_flow * max_flow_speed;
      
      	// if payload is empty - disable effect
      	if (current_payload < 0)
      		dropWaterEffect->setEmitterEnabled(false);
      }
      
      void WaterDropAircraftController::shutdown()
      {}
      
      /// Callback function to be executed on changing property parameters
      void WaterDropAircraftController::parameterChanged(Unigine::PropertyPtr prop, int propID)
      {
      	if (open.getID() == propID)
      		openWaterDropSystem(prop->getParameterPtr(propID)->getValueToggle());
      	else if (normalize_flow.getID() == propID)
      		setWaterDropSystemFlow(prop->getParameterPtr(propID)->getValueFloat());
      	else if (normalize_payload.getID() == propID)
      		setWaterPayload(prop->getParameterPtr(propID)->getValueFloat());
      }
      
      void WaterDropAircraftController::openWaterDropSystem(bool value)
      {
      	if (dropWaterEffect)
      		dropWaterEffect->setEmitterEnabled(value);
      }
      
      void WaterDropAircraftController::setWaterDropSystemFlow(float value)
      {
      	if (dropWaterEffect)
      		dropWaterEffect->setSpawnRate(value * spawn_rate_factor.get());
      }
      
      void WaterDropAircraftController::setWaterPayload(float value)
      {
      	current_payload = value * max_water_payload;
      }

    In the header file (WaterDropAircraftController.h) define the name for the property file and describe parameters of the component:

    WaterDropAircraftController.h

    Source code (C++)
    // Specifying the name of the property file and parameters for our component
    PROP_NAME("WaterDropAircraftController");
    PROP_PARAM(Toggle, open, false, "Open", "Input parameter for enabling/disabling the effect", "Input");
    PROP_PARAM(Float, normalize_flow, 1.0f, "Normalize Flow", "Input parameter for the effect power normalization", "Input");
    PROP_PARAM(Float, normalize_payload, 1.0f, "Normalize Payload", "Input parameter for payload normalization", "Input");
    
    PROP_PARAM(Node, particles_system, "Particles System", "ObjectParticles with the effect", "Effect");
    PROP_PARAM(Float, spawn_rate_factor, 100.0f, "Spawn Rate Factor", "Multiplier for spawn rate", "Effect");
    
    PROP_PARAM(Toggle, controlled_payload_time, false, "Controlled Payload Time", "Enable: water effect can be stopped automatically", "Payload")
    PROP_PARAM(Float, max_water_payload, 100.0f, "Max Water Payload", "Full water payload in units (weight or volume)", "Payload")
    PROP_PARAM(Float, max_flow_speed, 1.0f, "Max Flow Speed", "Maximum water flow speed (units per second)", "Payload");

    In the implementation file (WaterDropAircraftController.cpp) write your component's logic. At the initialization stage subscribe to parameter change event, as this component (property) will be associated with IG components changing these parameters when receiving commands from a host.

    WaterDropAircraftController.cpp

    Source code (C++)
    using namespace Unigine;
    
    void WaterDropAircraftController::init()
    {
    
    	// Adding a callback on changing the property parameters
    	getProperty()->addCallback(Property::CALLBACK_PARAMETER_CHANGED,
    		MakeCallback(this, &WaterDropAircraftController::parameterChanged));
    
    }
  3. Build and launch your project. At the initialization stage the Component System will create a property file named WaterDropAircraftController.prop for our component.

    Source code (XML)
    <?xml version="1.0" encoding="utf-8"?>
    <property version="2.7.3.0" name="WaterDropAircraftController" manual="1" parent_name="node_base">
    	<parameter name="open" type="toggle" title="Open" tooltip="Input parameter for enabling/disabling the effect" group="Input">0</parameter>
    	<parameter name="normalize_flow" type="float" title="Normalize Flow" tooltip="Input parameter for the effect power normalization" group="Input">1</parameter>
    	<parameter name="normalize_payload" type="float" title="Normalize Payload" tooltip="Input parameter for payload normalization" group="Input">1</parameter>
    	<parameter name="particles_system" type="node" title="Particles System" tooltip="ObjectParticles with the effect" group="Effect">0</parameter>
    	<parameter name="spawn_rate_factor" type="float" title="Spawn Rate Factor" tooltip="Multiplier for spawn rate" group="Effect">100</parameter>
    	<parameter name="controlled_payload_time" type="toggle" title="Controlled Payload Time" tooltip="Enable: water effect can be stopped automatically" group="Payload">0</parameter>
    	<parameter name="max_water_payload" type="float" title="Max Water Payload" tooltip="Full water payload in units (weight or volume)" group="Payload">100</parameter>
    	<parameter name="max_flow_speed" type="float" title="Max Flow Speed" tooltip="Maximum water flow speed (units per second)" group="Payload">1</parameter>
    </property>
  4. Via the UNIGINE Editor, assign the new created property file (WaterDropAircraftController.prop) to a node, for which the component was created (Be-200 in our case).

    Create a Particle System in the UNIGINE Editor and set its parameters as required. Add this Particle System to the corresponding field of the property.

  5. Add the description of our component to the desired entity in the IG configuration file (ig_config.xml). Its name and parameters must correspond to the ones described in the component's header file (see Step 3).

    Source code (XML)
    <!-- ..... -->
    	<entity id="200" name="be-200">
    		<!-- ..... -->
    		<component id="6" name="water_drop">
                    <property>WaterDropAircraftController</property>
                    <parameter name="state">open</parameter>
                    <parameter name="data1">normalize_flow</parameter>
                    <parameter name="data2">normalize_payload</parameter>
    				<!-- ..... -->
    		</component>
    		<!-- ..... -->
    	</entity>
    <!-- ..... -->
  6. To test the new component, add its description to the configuration file of the host emulator (<path_to_host>/Default/Entities.def).

    Source code
    entity
    {
    	name = "Be-200";
    	type = 200;
    	class = fixedwing;
            ....
            component
    	{
    		name = "water_drop";
    		id = 6;
    		def_state = 0;
    		state
    		{
    			name = "close";
    			value = 0;
    		}
    		state
    		{
    			name = "open";
    			value = 1;
    		}
    	}
    }

After launching your host application or a CIGI Host Emulator you can control your custom component by sending the corresponding packets to the IG.

Create a Project using the IG Template and Component System

Creating a Custom Network Component#

Usually, if you need to render a world across several computers synchronized over LAN, you use Syncker. It sends huge amount of data each frame from Master to all Slaves. Using Syncker allows for precise image synchronization, however, it is also resource-consuming.

IG Template provides functionality allowing you to reduce the amount of sent data and, therefore, network load. For example, to rotate an object, you can simply send a rotation velocity from Master to Slaves one time.

To create a network component with custom application logic using the IG Template functionality, you should perform the same steps as if you create a simple component (described above). The main difference is in their implementation:

  • An IG network component is inherited from the Unigine::Plugins::IG::NetworkComponentBase class.
  • Component declaration requires the NET_COMPONENT_DEFINE macro.
  • Automatic registration of the network component requires the NET_REGISTER_COMPONENT macro.
Notice
The network component will behave like a simple component in case there is no network.

When implementing the network component, you define data to be transferred over the network and send it only when necessary: you specify methods that can be invoked over a network (i.e. they can be called on Slaves) via the NET_DEFINE_SLOT macro and then call them on Master via the NET_CALL macro passing the required data. These methods will be called on Slaves as well.

As an example, let's implement rotation of a node around the Z-axis synchronized over the network using the IG Template functionality. In our case, the RPM speed is enough for calculation of the rotation angle. Here are key moments on the implementation of the component:

  • On the component initialization (the init() function), there is no need to synchronize the node over the network. All changes are sent from Master to Slaves only if the RPM speed value is changed via the setRPM() function.
  • Updating of the component (the update() function) is performed on Master and all Slaves simultaneously. Here you calculate the current rotation angle of the node and update its trasformation. For calculation, the IG::Manager::getIFps() method is used: unlike the Game::getIFps() method, it implements more accurate frame time calculation (including spike and freeze periods).
  • When the RPM speed value is changed on Master, the setRPM() method of the component sends it to all Slaves and the setRPM() is called on all Slaves. For the setRPM() to be invoked over the network, use the NET_CALL macro. It has an effect only on Master, so you can leave this code on Slaves - it won't be invoked.
    Notice
    You cannot send data from Slaves to Master.

Follow the instructions given below to create a network component described above:

  1. Create a new project using the IG Template as described here. Make sure that the Syncker plugin is included in the project (the Plugins button in the Create New Project window) as the IG Template uses it as the network for transferring commands.

  2. Create the following files implementing the RotatorIGNetwork component:
    Notice

    If you have ExtraSlaves, which can be connected after the simulation has started, and you want to synchronize your component (its internal state, any parameters, commands, etc.), you should override the following methods:

    • saveState() that writes the component data to be synchronized to a blob on Master. Try to write the minimum data that describes the full state.
    • restoreState() that reads the data from the blob on Slaves.

    NetworkRotator.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <plugins/Unigine/IG/UnigineIGNetwork.h>
    
    // synchronization of rotation using the IG Template functionality
    class RotatorIGNetwork : public Unigine::Plugins::IG::NetworkComponentBase
    {
    public:
    	// declare constructor and destructor for the component and define a property name
    	NET_COMPONENT_DEFINE(RotatorIGNetwork, Unigine::Plugins::IG::NetworkComponentBase);
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	// define that the setRPM() function can be invoked over the network
    	NET_DEFINE_SLOT(setRPM); 
    	// specify the RPM speed for a node
    	void setRPM(float speed);
    	// return the current RPM speed of a node
    	float getRPM() const;
    	
    	// saving and restoring the full state of the component is required when you have ExtraSlaves that can be connected with delay
    	// try to write the minimum information on the state
    	// saveState() is called on the Master when the ExtraSlave is connected
    	virtual void saveState(const Unigine::BlobPtr &blob) const override;
    	// restoreState() is called on the Slave connected with delay
    	virtual void restoreState(const Unigine::BlobPtr &blob) override;
    
    private:
    	Unigine::Math::Mat4 init_transform;
    	float rpm_speed = 0.0f;
    	double current_angle = 0.0f;
    
    	void init();
    	void update();
    };

    NetworkRotator.cpp

    Source code (C++)
    #include "NetworkRotator.h"
    #include <UnigineGame.h>
    // register a new network component
    NET_REGISTER_COMPONENT(RotatorIGNetwork);
    
    // update the RPM speed
    void RotatorIGNetwork::setRPM(float speed)
    {
    
    	rpm_speed = speed;
    	
    	// if the RPM speed value is changed on the Master, send the RPM speed value from the Master to all Slaves
    	NET_CALL(setRPM, speed);
    }
    
    // get the current RPM speed
    float RotatorIGNetwork::getRPM() const
    {
    
    	return rpm_speed;
    }
    
    // save the current RPM speed and the rotation angle into a blob
    void RotatorIGNetwork::saveState(const Unigine::BlobPtr &blob) const
    {
    	blob->write(rpm_speed);
    	blob->write(current_angle);
    }
    
    // restore the RMP speed and the rotation angle from the blob
    void RotatorIGNetwork::restoreState(const Unigine::BlobPtr &blob)
    {
    	blob->read(rpm_speed);
    	blob->read(current_angle);
    }
    
    void RotatorIGNetwork::init()
    {
    	
    	// get the node transformation
    	init_transform = node->getTransform();
    }
    
    // update() is executed on the Master and all Slaves simultaneously
    void RotatorIGNetwork::update()
    {
    	// calculate the current rotation angle of the node
    	current_angle += Unigine::Plugins::IG::Manager::get()->getIFps() * rpm_speed * 360.0;
    	// update node transformation
    	node->setTransform(init_transform * Unigine::Math::rotateZ(current_angle));
    }
    Notice
    You can also use the IG_ONLY_FOR_MASTER and IG_ONLY_FOR_SLAVE macros to define where the logic should be executed.
  3. Build and launch your project. At the initialization stage, a property file named RotatorIGNetwork.prop is created.
  4. In the UNIGINE Editor, assign the created property file to a node, for which the component was created.
  5. Save changes and run your application.
Last update: 2023-12-19
Build: ()