UnigineEditor
Interface Overview
Assets Workflow
Settings and Preferences
Working With Projects
Adjusting Node Parameters
Setting Up Materials
Setting Up Properties
Landscape Tool
Using Editor Tools for Specific Tasks
Extending Editor Functionality
编程
Fundamentals
Setting Up Development Environment
Usage Examples
UnigineScript
C++
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

Migrating to UNIGINE from Unreal Engine: Programming

Game logic in an Unreal Engine project is implemented via Blueprint Script / C++ components. You got used to determine actor's behavior by writing event functions like InitializeComponent() and TickComponent().

In UNIGINE, you can create projects based on C++, C# and UnigineScript API. A visual scripting feature is being under research and development at the moment.

The traditional workflow implies the Application Logic has three basic components that have different lifetimes:

  • SystemLogic (the AppSystemLogic.cpp source file) exists during the application life cycle.
  • WorldLogic (the AppWorldLogic.cpp source file) takes effect only when a world is loaded.
  • EditorLogic (the AppEditorLogic.cpp source file) takes effect only when a custom editor is loaded (there is a class derived from the EditorLogic class).

Check out the Programming Quick Start series to get started in traditional C++ programming in UNIGINE.

Regarding components, UNIGINE has quite a similar concept, which can be easily adopted — Custom C++ Component System, which is safe and secure and ensures high performance. Logic is written in C++ classes derived from the ComponentBase class, based on which the Engine generates a set of component's parameters — Property that can be assigned to any node in the scene. Each component has a set of functions (init(), update(), etc.), that are called by the corresponding functions of the Engine's main loop.

From here on this articles will cover primarily programming in C++ using the Custom C++ Component System as a more natural and familiar workflow for Unreal Engine users. Check out the Using Custom C++ Component System article for an example for beginners.

Programming in UNIGINE using C++ is not much different from programming in Unreal Engine except that you need to make some preparations and create headers files for components as in the natural coding in C++. For example, let's compare how simple components are created in both engines. In UE4:

Source code (C++)
UCLASS()
class UMyComponent : public UActorComponent
{
    GENERATED_BODY()

    // Called after the owning Actor was created
    void InitializeComponent();

    // Called when the component or the owning Actor is being destroyed
    void UninitializeComponent();

    // Component version of Tick
    void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction);
};

And in UNIGINE. To make the things work, you need to initialize the Custom Component System in the AppSystemLogic.cpp file:

Source code (C++)
/* .. */
#include "ComponentSystem/ComponentSystem.h"

/* .. */

int AppSystemLogic::init()
{
	// Write here code to be called on engine initialization.
	ComponentSystem::get()->initialize();
	return 1;
}

And then you can write a new component.
MyComponent.h:

Source code (C++)
#pragma once
#include <Unigine.h>
#include "ComponentSystem/ComponentSystem.h"

using namespace Unigine;
class MyComponent : public ComponentBase
{
public:
	// declare the component
	COMPONENT(MyComponent, ComponentBase);

	// declare methods to be called at the corresponding stages of the execution sequence
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);
	COMPONENT_SHUTDOWN(shutdown);

	// declare the name of the property to be used in the Editor
	PROP_NAME("my_component");
protected:
	void init();
	void update();
	void shutdown();
};
MyComponent.cpp:
Source code (C++)
#include "MyComponent.h"

// register the component in the Component System
REGISTER_COMPONENT(MyComponent);

// called on component initialization
void MyComponent::init(){}

// called every frame
void MyComponent::update(){}

// called on component or the owning node is being destroyed
void MyComponent::shutdown(){}

After that, you will need to perform the following steps:

  1. Build the application using the IDE.
  2. Run the application once to get the component Property generated by the engine.
  3. Assign the property to a node.
  4. Finally, you can check out it's work by launching the application.

To learn more about the execution sequence and how to build components, follow the links below:

For those who prefer C#, UNIGINE allows creating C# applications using C# API and, if required, C# Component System.

Writing Gameplay Code#

Printing to Console#

Unreal Engine UNIGINE
Source code (C++)
UE_LOG(LogTemp, Warning, TEXT("Your message"));
Source code (C++)
Log::message("Debug info: %s\n", text);
Log::message("Debug info: %d\n", number);

See Also#

  • More types of messages in the Log class API.
  • The video tutorial demonstrating how to print user messages to console using C# Component System.

Loading a Scene#

Unreal Engine UNIGINE
Source code (C++)
UGameplayStatics::OpenLevel(GetWorld(), TEXT("MyLevelName"));
Source code (C++)
World.LoadWorld("YourSceneName");

Accessing Actor / Node from Component#

Unreal Engine UNIGINE
Source code (C++)
MyComponent->GetOwner();
Source code (C++)
NodePtr owning_node = node;

See Also#

  • The video tutorial demonstrating how to access nodes from components using C# Component System.

Accessing a Component from the Actor / Node#

Unreal Engine:

Source code (C++)
UMyComponent* MyComp = MyActor->FindComponentByClass<UMyComponent>();

UNIGINE:

Source code (C++)
MyComponent *my_component = getComponent<MyComponent>(node);

Finding Actors / Nodes#

Unreal Engine:

Source code (C++)
// Find Actor by name (also works on UObjects)
AActor* MyActor = FindObject<AActor>(nullptr, TEXT("MyNamedActor"));

// Find Actors by type (needs a UWorld object)
for (TActorIterator<AMyActor> It(GetWorld()); It; ++It)
{
        AMyActor* MyActor = *It;
        // ...
}

UNIGINE:

Source code (C++)
// Find a Node by name
NodePtr my_node = World::getNodeByName("my_node");

// Find all nodes having this name
Vector<NodePtr> nodes;
World::getNodesByName("test", nodes);

// Find the index of a direct child node
int index = node->findChild("child_node");
NodePtr direct_child = node->getChild(index);

// Perform a recursive descend down the hierarchy to find a child Node by name
NodePtr child = node->findNode("child_node", 1);

Casting From Type to Type#

Downcasting (from a pointer-to-base to a pointer-to-derived) is performed using different constructions. To perform Upcasting (from a pointer-to-derived to a pointer-to-base) you can simply use the instance itself.

Unreal Engine:

Source code (C++)
UPrimitiveComponent* Primitive = MyActor->GetComponentByClass(UPrimitiveComponent::StaticClass());
USphereComponent* SphereCollider = Cast<USphereComponent>(Primitive);
if (SphereCollider != nullptr)
{
        // ...
}

UNIGINE:

Source code (C++)
// find a pointer to node by a given name
NodePtr baseptr = World::getNodeByName("my_meshdynamic");

// cast a pointer-to-derived from pointer-to-base with automatic type checking
ObjectMeshDynamicPtr derivedptr = checked_ptr_cast<ObjectMeshDynamic>(baseptr);

// static cast (pointer-to-derived from pointer-to-base)
ObjectMeshDynamicPtr derivedptr = static_ptr_cast<ObjectMeshDynamic>(World::getNodeByName("my_meshdynamic"));

// upcast to the pointer to the Object class which is a base class for ObjectMeshDynamic
ObjectPtr object = derivedptr;

// upcast to the pointer to the Node class which is a base class for all scene objects
NodePtr node = derivedptr;

Destroy Actor / Node#

Unreal Engine UNIGINE
Source code (C++)
MyActor->Destroy();

// destroy the actor with 1 second delay
MyActor->SetLifeSpan(1);
Source code (C++)
node.deleteLater(); // recommended option
//called between the current and the next frames

node.deleteForce(); // called during the same frame but unsafe

To perform deferred removal of a node in UNIGINE, you can create a component that will be responsible for the timer and deletion.

Instantiating Actor / Node Reference#

In UE4, you create a clone of an actor the following way:

Source code (C++)
AMyActor* CreateCloneOfMyActor(AMyActor* ExistingActor, FVector SpawnLocation, FRotator SpawnRotation)
{
    UWorld* World = ExistingActor->GetWorld();
    FActorSpawnParameters SpawnParams;
    SpawnParams.Template = ExistingActor;
    World->SpawnActor<AMyActor>(ExistingActor->GetClass(), SpawnLocation, SpawnRotation, SpawnParams);
}

In UNIGINE, you should use World::loadNode to load a hierarchy of nodes from a .node asset. In this case the hierarchy of nodes that was saved as a NodeReference will be added to the scene. You can refer to the asset either via a component parameter or manually by providing the virtual path to it:

Source code (C++)
// MyComponent.h
PROP_PARAM(File, node_to_spawn);
Source code (C++)
// MyComponent.cpp
/* .. */
void MyComponent::init()
{
	// load a hierarchy of nodes from the asset
	NodePtr spawned = World::loadNode(node_to_spawn.get());
	spawned->setWorldPosition(node->getWorldPosition());

	NodePtr spawned_manually = World::loadNode("nodes/node_reference.node");
}

In case of using the approach of component parameters, you should also specify the .node asset:

You can also spawn the node reference as a single node (without extracting the content) in the world:

Source code (C++)
void MyComponent::update()
{
	NodeReferencePtr nodeRef = NodeReference::create("nodes/node_reference_0.node");
}

Triggers#

Unreal Engine:

Source code (C++)
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    // My trigger component
    UPROPERTY()
    UPrimitiveComponent* Trigger;

    AMyActor()
    {
        Trigger = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerCollider"));

        // Both colliders need to have this set to true for events to fire
        Trigger.bGenerateOverlapEvents = true;

        // Set the collision mode for the collider
        // This mode will only enable the collider for raycasts, sweeps, and overlaps
        Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    }

    virtual void NotifyActorBeginOverlap(AActor* Other) override;

    virtual void NotifyActorEndOverlap(AActor* Other) override;
};

In UNIGINE, triggers are special built-in node types that raise events in certain situations:

The WorldTriger is the most common type that can be used in gameplay. Here is an example on how to use it:

Source code (C++)
WorldTriggerPtr trigger;
int enter_callback_id;
				
// implement the enter callback
void AppWorldLogic::enter_callback(NodePtr node){
	Log::message("\nA node named %s has entered the trigger\n", node->getName());
}
				
// implement the leave callback
void AppWorldLogic::leave_callback(NodePtr node){
	Log::message("\nA node named %s has left the trigger\n", node->getName());
}
				
int AppWorldLogic::init() {
	// create a world trigger node
	trigger = WorldTrigger::create(Math::vec3(3.0f));
				
	// add the enter callback to be fired when a node enters the world trigger
	//and keep its id to be used to remove the callback when necessary
	enter_callback_id = trigger->addEnterCallback(MakeCallback(this, &AppWorldLogic::enter_callback));
	// add the leave callback to be fired when a node leaves the world trigger
	trigger->addLeaveCallback(MakeCallback(this, &AppWorldLogic::leave_callback));

	return 1;
}

Input#

UE4 Input:

Source code (C++)
UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

    void SetupInputComponent()
    {
        Super::SetupInputComponent();

        InputComponent->BindAction("Fire", IE_Pressed, this, &AMyPlayerController::HandleFireInputEvent);
        InputComponent->BindAxis("Horizontal", this, &AMyPlayerController::HandleHorizontalAxisInputEvent);
        InputComponent->BindAxis("Vertical", this, &AMyPlayerController::HandleVerticalAxisInputEvent);
    }

    void HandleFireInputEvent();
    void HandleHorizontalAxisInputEvent(float Value);
    void HandleVerticalAxisInputEvent(float Value);
};

UNIGINE:

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

#include <UnigineApp.h>
#include <UnigineConsole.h>
#include <UnigineInput.h>

/* .. */

void MyInputController::update() 
{
	// if right mouse button is clicked
	if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
	{
		Math::ivec2 mouse = Input::getMouseCoord();
		// report mouse cursor coordinates to the console
		Log::message("Right mouse button was clicked at (%d, %d)\n", mouse.x, mouse.y);
	}
	
	// closing the application if a 'Q' key is pressed, ignoring the key if the console is opened
	if (Input::isKeyDown(Input::KEY_Q) && !Console::getActivity())
	{
		App::exit();
	}
}

/* .. */

You can also use the ControlsApp class to handle control bindings. To configure the bindings, open the Controls settings:

Source code (C++)
#include <Unigine.h>

/* .. */

void MyInputController::init() 
{
	// remapping states to other keys and buttons
	ControlsApp::setStateKey(Controls::STATE_FORWARD, App::KEY_PGUP);
	ControlsApp::setStateKey(Controls::STATE_BACKWARD, App::KEY_PGDOWN);
	ControlsApp::setStateKey(Controls::STATE_MOVE_LEFT, 'l');
	ControlsApp::setStateKey(Controls::STATE_MOVE_RIGHT, 'r');
	ControlsApp::setStateButton(Controls::STATE_JUMP, App::BUTTON_LEFT);
}

void MyInputController::update() 
{
	if (ControlsApp::clearState(Controls::STATE_FORWARD))
	{
		Log::message("FORWARD key pressed\n");
	}
	else if (ControlsApp::clearState(Controls::STATE_BACKWARD))
	{
		Log::message("BACKWARD key pressed\n");
	}
	else if (ControlsApp::clearState(Controls::STATE_MOVE_LEFT))
	{
		Log::message("MOVE_LEFT key pressed\n");
	}
	else if (ControlsApp::clearState(Controls::STATE_MOVE_RIGHT))
	{
		Log::message("MOVE_RIGHT key pressed\n");
	}
	else if (ControlsApp::clearState(Controls::STATE_JUMP))
	{
		Log::message("JUMP button pressed\n");
	}
}

/* .. */

Ray Tracing#

Unreal Engine:

Source code (C++)
APawn* AMyPlayerController::FindPawnCameraIsLookingAt()
{
    // You can use this to customize various properties about the trace
    FCollisionQueryParams Params;
    // Ignore the player's pawn
    Params.AddIgnoredActor(GetPawn());

    // The hit result gets populated by the line trace
    FHitResult Hit;

    // Raycast out from the camera, only collide with pawns (they are on the ECC_Pawn collision channel)
    FVector Start = PlayerCameraManager->GetCameraLocation();
    FVector End = Start + (PlayerCameraManager->GetCameraRotation().Vector() * 1000.0f);
    bool bHit = GetWorld()->LineTraceSingle(Hit, Start, End, ECC_Pawn, Params);

    if (bHit)
    {
        // Hit.Actor contains a weak pointer to the Actor that the trace hit
        return Cast<APawn>(Hit.Actor.Get());
    }

    return nullptr;
}

In UNIGINE the same is handled by Intersections:

Source code (C++)
#include "MyComponent.h"
#include <UnigineWorld.h>
#include <UnigineVisualizer.h>
#include <UnigineGame.h>
#include <UnigineInput.h>

using namespace Unigine;
using namespace Math;

REGISTER_COMPONENT(MyComponent);

void MyComponent::init()
{
	Visualizer::setEnabled(true);
}

void MyComponent::update()
{
	ivec2 mouse = Input::getMouseCoord();
	float length = 100.0f;
	vec3 start = Game::getPlayer()->getWorldPosition();
	vec3 end = start + vec3(Game::getPlayer()->getDirectionFromScreen(mouse.x, mouse.y)) * length;

	// ignore surfaces that have certain bits of the Intersection mask enabled
	int mask = ~(1 << 2 | 1 << 4);

	WorldIntersectionNormalPtr intersection = WorldIntersectionNormal::create();

	ObjectPtr obj = World::getIntersection(start, end, mask, intersection);

	if (obj)
	{
		vec3 point = intersection->getPoint();
		vec3 normal = intersection->getNormal();
		Visualizer::renderVector(point, point + normal, vec4_one);
		Log::message("Hit %s at (%f,%f,%f)\n", obj->getName(), point.x, point.y, point.z);
	}
}
Last update: 2020-05-19