UnigineEditor
界面概述
资产工作流程
设置和首选项
项目开发
调整节点参数
Setting Up Materials
Setting Up Properties
照明
Landscape Tool
Sandworm (Experimental)
使用编辑器工具执行特定任务
Extending Editor Functionality
编程
基本原理
搭建开发环境
Usage Examples
UnigineScript
C++
C#
UUSL (Unified UNIGINE Shader Language)
File Formats
Rebuilding the Engine Tools
GUI
双精度坐标
应用程序接口
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 — 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 article covers primarily programming in C++ using the C++ Component System as a more natural and familiar workflow for Unreal Engine users. Check out the Using 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 Component System in the AppSystemLogic.cpp file:

Source code (C++)
/* .. */
#include <UnigineComponentSystem.h>

/* .. */

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

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

Source code (C++)
#pragma once
#include <Unigine.h>
#include <UnigineComponentSystem.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 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 its 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-07-24