Creating Custom Components
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.
-
Create a new project using the IG Template as described here.
-
Create the following files describing your new component to the C++ project:
-
WaterDropAircraftController.h
#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
#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
// 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
using namespace Unigine; void WaterDropAircraftController::init() { // Adding a callback on changing the property parameters getProperty()->addCallback(Property::CALLBACK_PARAMETER_CHANGED, MakeCallback(this, &WaterDropAircraftController::parameterChanged)); }
-
-
Build and launch your project. At the initialization stage the Component System will create a property file named WaterDropAircraftController.prop for our component.
<?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>
-
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.
-
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).
<!-- ..... --> <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> <!-- ..... -->
-
To test the new component, add its description to the configuration file of the host emulator (<path_to_host>/Default/Entities.def).
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.
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.
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.
You cannot send data from Slaves to Master.
Follow the instructions given below to create a network component described above:
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.
- Create the following files implementing the RotatorIGNetwork component:
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
#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
#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)); }
You can also use the IG_ONLY_FOR_MASTER and IG_ONLY_FOR_SLAVE macros to define where the logic should be executed. - Build and launch your project. At the initialization stage, a property file named RotatorIGNetwork.prop is created.
- In the UNIGINE Editor, assign the created property file to a node, for which the component was created.
- Save changes and run your application.