UnigineEditor
Interface Overview
Assets Workflow
Settings and Preferences
Working With Projects
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Lighting
Landscape Tool
Using Editor Tools for Specific Tasks
Extending Editor Functionality
编程
Fundamentals
Setting Up Development Environment
Usage Examples
UnigineScript
C#
UUSL (Unified UNIGINE Shader Language)
File Formats
Rebuilding the Engine Tools
GUI
Double Precision Coordinates
应用程序接口
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

Implementing Syncker Logic for a Custom Project

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.

C++ Implementation#

You can create separate Master and Slave applications, or a single one to run on both Master and Slave sides. To enable the Syncker in your Master or Slave application to synchronize nodes, render parameters and players over the network, follow the instructions below.

Initializing Syncker#

First of all, you should initialize Syncker. To do so you can simply add the following code to the AppSystemLogic class for both Master and Slave applications.

Insert the following Syncker initialization code to the AppSystemLogic::init() method:

Source code (C++)
// AppSystemLogic.cpp

#include "AppSystemLogic.h"
#include <UnigineApp.h>
#include <plugins/UnigineSyncker.h>

/* .. */

using namespace Unigine;
using namespace Plugins;

/* .. */

int AppSystemLogic::init() 
{
	// update application even if focus was lost
	App::setBackgroundUpdate(true);

	// get the Syncker manager interface
	Syncker::Manager *syncker_manager = Syncker::Manager::get();

	// initialize the Syncker using command line arguments
	// or you can initialize Syncker as Master or Slave directly
	//	via initMaster() or initSlave() methods without using command-line arguments
	syncker_manager->initSyncker();

	// enable debug information in right bottom corner
	syncker_manager->getSyncker()->setDebug(true);

	return 1;
}

In the AppWorldLogic.cpp file add the code to get the pointer to the manager and Master interface for further use in the AppWorldLogic::init() method:

Source code (C++)
// AppWorldLogic.cpp
#include <UnigineEditor.h>
#include <UnigineGame.h>
#include <UnigineApp.h>

/* .. */

using namespace Unigine;
using namespace Syncker;
using namespace Math;

int AppWorldLogic::init() 
{
	Syncker::Manager *syncker_manager = Syncker::Manager::get();
	if (syncker_manager && syncker_manager->isMasterInitialized())
		syncker_master = syncker_manager->getMaster();


	return 1;
}

Synchronizing Nodes/Materials#

Objects of the following types are synchronized automatically: ObjectWaterGlobal, ObjectCloudLayer, ObjectParticles, WorldLight if they present in the *.world file on all computers.

Synchronization of transformations is supported for all types of nodes. As for other type-specific parameters synchronization is available only for the following ones:

To start synchronizing other nodes or materials (the logic is the same for both), that exist in the world, you just need to tell the Syncker which of them you want. To do so you can add the following code to the AppWorldLogic::init() method of your master application:

Source code (C++)
int AppWorldLogic::init() 
{
	/* ... */

	// checking if we have a Master interface
	if (syncker_master)
	{
		// getting a pointer for the node named "material_ball"
		NodePtr my_node = World::getNodeByName("material_ball");

		// add the existing node to synchronization
		syncker_master->addSyncNode(my_node);
	}

	return 1;
}

Now, you can implement your logic for synchronized nodes in the AppWorldLogic::update() method for the Master:

Source code (C++)
int AppWorldLogic::update() 
{
	/* ... */

	// checking if we have a Master interface
	if (syncker_master)
	{
		// getting a pointer for the node named "material_ball"
		NodePtr my_node = World::getNodeByName("material_ball");
		
		// changing node's rotation
		my_node->setRotation(my_node->getRotation() * Math::quat(0, 0, Game::getIFps()));

	}

	return 1;
}

Creating/Deleting Nodes at Runtime#

You can create and delete nodes at run time. To synchronize creation of a node on all connected Slaves, use the createNode() function.

Notice
Please note, that the createNode() method support only a limited number of node types, so it is recomended to load nodes from *.node files instead.
Source code (C++)
/* ... */
int cube_created = 0;

int AppWorldLogic::update() 
{
	/* ... */

	// checking if we have a Master interface and cube is not yet created
	if (syncker_master && !cube_created)
	{
		// creating a dynamic cube and set up its material and transformation
		ObjectMeshStaticPtr dynamic_cube = ObjectMeshStatic::create("box.mesh");
		dynamic_cube->setMaterial("mesh_base", 0);
		dynamic_cube->setPosition(Math::Vec3(game->getRandomFloat(-50, 50), game->getRandomFloat(-50, 50), game->getRandomFloat(0, 50)));
		dynamic_cube->setRotation(Math::quat(game->getRandomFloat(0, 360), game->getRandomFloat(0, 360), game->getRandomFloat(0, 360)));
			
		// commanding Slaves to create a dynamic node and adding it to synchronization
		syncker_master->createNode(dynamic_cube);

		cube_created = 1;
	}

	return 1;
}
Notice
For this code to be compiled, copy the box.mesh file from the <Unigine SDK>/data/samples/common/meshes folder to the <your_unigine_project>/data/<your_unigine_project>/meshes folder.

Loading Nodes at Runtime#

To synchronize node loading from a *.node file on all connected Slaves, use the loadNode() or loadNodereference() methods. This approach is recommended as it allows adding nodes of all types, unlike the createNode() method that supports only a limited number of them.

Source code (C++)
/* ... */
int node_loaded = 0;

int AppWorldLogic::update() 
{
	/* ... */

	// checking if we have a Master interface and our node is not yet loaded
	if (syncker_master && !node_loaded)
	{
			
		// commanding Slaves to load a node and adding it to synchronization
		syncker_master->loadNode("my_node.node");

		node_loaded = 1;
	}

	return 1;
}

Customizing Synchronization via User Messages#

Let us consider the following simple example: we can set transformation for a node, by simply sending rotation and position via a user message.

  1. First step is to create an object on the Master and on all Slaves as a static one (without adding it to synchronization).
  2. Next, we implement a callback function:
    • on_message_received - to be called on receiving a user message. Here we extract the parameters from the received message and use them (in this case to change node's transformation).
  3. And the last step is to set this callback using the setMessageReceivedCallback() method in the AppWorldLogic::init()
    Source code (C++)
    // declaring and initializing pointers for the Master, Slave, and Syncker instances
    Unigine::Plugins::Syncker::Master *master = nullptr;
    Unigine::Plugins::Syncker::Slave *slave = nullptr;
    Unigine::Plugins::Syncker::Syncker *syncker = nullptr;
    
    // declaring a node, that we are going to transform via user messages
    NodePtr node;
    
    // declaring a blob to store our messages
    Unigine::BlobPtr blob = Unigine::Blob::create();
    
    // declaring message types to be used
    enum MESSAGE_TYPE
    	{
    		REPORT,
    		TRANSFORM,
    	};
    
    /* ... */
    
    
    /// callback function to be fired on receiving a user message
    void AppWorldLogic::on_message_received(const Unigine::BlobPtr &message)
    {
    	// reading an unsigned char from the blob defining message type (REPORT or TRANSFORM)
    	unsigned char type = message->readUChar();
    	switch (type)
    	{
    		case REPORT:
    		{	
    			// printing to the console that we have received a message
    			Log::message("REPORT Message received!")
    		}
    		break;
    
    		case TRANSFORM:
    		{
    			// reading position and rotation from the message and using them to update node's transformation
    			Vec3 pos = Vec3(message->readDVec3());
    			quat rot = message->readQuat();
    			node->setTransform(translate(pos) * Mat4(rotate(rot)));
    		}
    		break;
    	}
    }
    
    int AppWorldLogic::init() 
    {
    	/* ... */
    	
    	// trying to get a Manager interface
    	auto manager = Syncker::Manager::get();
    	if (manager)
    	{	
    		// checking if we have a Master interface
    		if (manager->isMasterInitialized())
    			master = manager->getMaster();
    		else if (manager->isSlaveInitialized())
    			slave = manager->getSlave();
    
    		// getting a pointer to Master/Slave base class
    		syncker = manager->getSyncker();
    	}
    
    	// subscribing to network messages
    	if (syncker)
    		syncker->setMessageReceivedCallback("net", MakeCallback(this, &AppWorldLogic::on_message_received));
    	
    	/* ... */
    	
    	// somwhere in code... sending a REPORT message
    	blob->clear();
    	blob->writeUChar(REPORT);
    	syncker->sendMessage("net", blob);
    	return 1;
    }
    
    int AppWorldLogic::update() 
    {
    	/* ... */
    	
    	// sending a message with object's transform to all peers
    	if (master)
    	{
    		Vec3 pos = node->getPosition();
    		quat rot = node->getRotation();
    		blob->clear();
    		blob->writeUChar(TRANSFORM);
    		blob->writeDVec3(dvec3(pos));
    		blob->writeQuat(rot);
    		syncker->sendMessage("net", blob);
    	}
    	
    	/* ... */
    	
    	return 1;
    }

Changing Views and Cameras#

You can change the viewport on the Master or Slave side. For this purpose you can use the following code (example):

Source code (C++)
Unigine::Plugins::Syncker::Manager* syncker_manager;
/* ... 

perform checks if the Syncker plugin is loaded 
and get the Syncker manager interface (Manager)

 ... */

// if syncker is initialized, assigning the config mesh surface named "surface_0" to the 2nd monitor
if (syncker_manager->isSynckerIntialized()) 
	syncker_manager->getSyncker()->setView(1, "surface_0");
	
/* ... */

You can also tell a Slave to use another camera. To do so, you can use the following code (example):

Source code (C++)
/* ... */

PlayerSpectatorPtr aux_player = nullptr;

/* ... */

int AppWorldLogic::update() 
{
	/* ... */
	
	// checking if we have a Master interface
	if (syncker_master && !aux_player)
	{
		// creating a new auxiliary player (on Master and Slaves)
		aux_player = PlayerSpectator::create();
		aux_player->setPosition(Vec3(0.0f, -40.0f, 200.0f));
		aux_player->setDirection(vec3(0.0f, 1.0f, 0.0f), vec3_up);
		aux_player->setMinVelocity(10);
		aux_player->setMaxVelocity(aux_player->getMinVelocity() * 2);
	
		// commanding Slaves to create a new player and adding it to synchronization
		syncker_master->createNode(aux_player);
		
		// setting a new created player for Slave(s) having sync_view = "view_1"
		syncker_master->setSlavePlayer("view_1", aux_player);
	}
	
/* ... */
}
Last update: 2020-07-28