Переход на UNIGINE с Unreal Engine 4: гайд для программистов
Написание игровой логики, триггеры, ввод, рейкастинг и другое.
Специально для тех, кто ищет альтернативу Unreal Engine или Unity, мы продолжаем цикл статей про безболезненный переход на UNIGINE с зарубежных движков. В третьем выпуске рассмотрим миграцию с Unreal Engine 4 с точки зрения программиста.
Общая информация
Игровая логика в проекте на Unreal Engine 4 реализуется с помощью классов C++ или Blueprint Visual Scripting — встроенной системы визуального нодового программирования. Редактор Unreal Engine 4 позволяет создавать классы при помощи встроенного мастера классов (Class Wizard), выбрав нужный базовый тип.
В UNIGINE вы можете создавать проекты, используя C++ и C# API. При создании проекта просто выберите желаемое API и систему сборки:
В данной статье в основном затронем программирование на C++, т.к. полноценное программирование в Unreal Engine 4 возможно именно на этом языке.
Для C++ на выбор представлены готовые шаблоны проектов для следующих систем сборки:
Windows:
Linux:
Далее просто выберите Open Code IDE, чтобы перейти к разработке логики в выбранной IDE для C++ проектов:
В Unreal Engine 4 достаточно унаследовать класс от базовых типов Game Framework, таких как AActor, APawn, ACharacter и т.п., чтобы переопределить их поведение в стандартных методах BeginPlay (), Tick () и EndPlay () и получить пользовательский actor.
Компонентный подход подразумевает, что логика реализуется в пользовательских компонентах, назначаемых на actor«ы — классах, унаследованных от UActorComponent и других компонентов, расширяющих стандартное поведение, определенное в методахInitializeComponent () и TickComponent ().
В UNIGINE стандартный подход подразумевает, что логика приложения состоит из трех основных компонентов с разным циклом жизни:
Системная логика (исходный файл AppSystemLogic.cpp) существует в течение жизненного цикла приложения.
Логика мира (исходный файл AppWorldLogic.cpp) выполняется только когда мир загружен.
Логика редактора (исходный файл AppEditorLogic.cpp) выполняется только во время работы пользовательского редактора.
У каждой логики есть стандартные методы, вызываемые в основном цикле движка. К примеру, можно использовать следующие методы логики мира:
init () — для инициализации ресурсов при загрузке мира;
update () — для обновления каждый кадр;
shutdown () — для уничтожения использованных ресурсов при закрытии мира;
Следует учитывать, что логика мира не привязана к конкретному миру и будет вызвана для любого загруженного мира. Однако вы можете разделить специфичный для мира код между отдельными классами, унаследованными от WorldLogic.
Компонентный подход также доступен в UNIGINE при помощи встроенной компонентной системы. Логика компонента определяется в классе, производном от ComponentBase, на основе которого движок сгенерирует набор параметров компонента — Property, которые можно назначить любой ноде в редакторе. Каждый компонент также имеет набор методов, которые вызываются соответствующими функциями основного цикла движка.
Для примера создания простой игры с использованием компонентной системы, обратитесь к серии статей «Краткое руководство по программированию».
Сравним, как создаются простые компоненты в обоих движках. Заголовочный файл компонента в Unreal Engine 4 будет выглядеть примерно так:
UCLASS()
class UMyComponent : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 TotalDamage;
// 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);
};
И в UNIGINE. Компонентную систему сперва необходимо инициализировать в системной логике (AppSystemLogic.cpp):
/* .. */
#include
/* .. */
int AppSystemLogic::init()
{
Unigine::ComponentSystem::get()->initialize();
return 1;
}
И тогда можно написать новый компонент:
MyComponent.h:
#pragma once
#include
#include
using namespace Unigine;
class MyComponent : public ComponentBase
{
public:
// объявление компонента MyComponent
COMPONENT(MyComponent, ComponentBase);
// объявление методов, вызываемых на определенных этапах цикла жизни компонента
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
COMPONENT_SHUTDOWN(shutdown);
// объявление параметра компонента, который будет доступен в редакторе
PROP_PARAM(Float, speed, 30.0f);
// определение имени Property, которое будет сгенерировано и ассоциировано с компонентом
PROP_NAME("my_component");
protected:
void init();
void update();
void shutdown();
};
MyComponent.cpp:
#include "MyComponent.h"
// регистрация компонента MyComponent
REGISTER_COMPONENT(MyComponent);
// вызов будет произведен при инициализации компонента
void MyComponent::init(){}
// будет вызван каждый кадр
void MyComponent::update(){}
// будет вызван при уничтожении компонента или ноды, которой он назначен
void MyComponent::shutdown(){}
Теперь необходимо сгенерировать property для нашего компонента. Для этого:
Соберите приложение с помощью IDE.
Запустите приложение один раз, чтобы получить property компонента, сгенерированное движком.
Перейдите в редактор и назначьте сгенерированное property ноде.
Наконец, работу логики компонента можно проверить, запустив приложение.
Чтобы узнать больше о последовательности выполнения и о том, как создавать компоненты, перейдите по ссылкам ниже:
Немного про API
Все объекты в Unreal Engine 4 наследуются от UObject, доступ к ним возможен при помощи стандартных C++ указателей или умных указателей Unreal Smart Pointer Library.
В UNIGINE API есть система умных указателей, управляющих существованием нод и других объектов в памяти:
// создать ноду типа NodeType
Ptr nodename = ::create();
// удалить ноду из мира
nodename.deleteLater();
К примеру, вот как выглядит создание меша из ассета, редактирование, присвоение новой ноде типа ObjectMeshStatic и удаление:
MeshPtr mesh = Mesh::create();
mesh->load("fbx/model.fbx/model.mesh");
mesh->addBoxSurface("box_surface", Math::vec3(0.5f, 0.5f, 0.5f));
ObjectMeshStaticPtr my_object = ObjectMeshStatic::create(mesh);
my_object.deleteLater();
mesh.clear();
Экземпляры пользовательских компонентов, как и любых других классов, хранятся при помощи стандартных указателей:
MyComponent *my_component = getComponent(node);
Типы данных
Тип данных | Unreal Engine 4 | UNIGINE |
Числовые типы | int8/uint8 int16/uint16 int32/uint32 int64/uint64, float, double | Стандартные типы C++: signed и unsigned char, short, int, long, long long, float, double |
Строки | FString: FString MyStr = TEXT («Hello, Unreal 4!»). | String: String str («Hello, UNIGINE 2!»); |
Контейнеры | TArray, TMap, TSet | Vector, Map, Set и другие: Vector World: getNodes (nodes); for (NodePtr n: nodes) { } |
Векторы и матрицы | FVector3f — FVector3d, FMatrix44f — FMatrtix44d и другие | vec3 — dvec3, mat4 — dmat4 и другие типы в математической библиотеке. |
UNIGINE поддерживает как одинарную точность (Float), так и двойную точность координат (Double), доступную в зависимости от редакции SDK. Почитайте про использование универсальных типов данных, подходящих под любой проект.
Основные примеры кода
Вывод в консоль
Unreal Engine 4 | UNIGINE |
|
|
См. также:
Загрузка сцены
Unreal Engine 4 | UNIGINE |
|
|
Доступ к Actor / Node из компонента
Unreal Engine 4 | UNIGINE |
| N |
См. также:
Видеоруководство, демонстрирующее, как получить доступ к нодам из компонентов с помощью C++ Component System.
Доступ к компоненту из Actor / Node
Unreal Engine 4:
UMyComponent* MyComp = MyActor->FindComponentByClass();
UNIGINE:
MyComponent *my_component = getComponent(node);
Работа с направлениями
В Unreal Engine 4 компонент USceneComponent (или производный) отвечает за действия с трансформацией actor«а. Чтобы получить вектор направления по одной из осей с учетом ориентации в мировых координатах, можно использовать соответствующие методы USceneComponent (GetForwardVector ()) или AActor (GetActorForwardVector ()).
В UNIGINE трансформация ноды в пространстве представлена ее матрицей трансформации (mat4), а все основные операции с трансформацией или иерархией нод доступны при помощи методов класса Node. Такой же вектор направления в UNIGINE получается с помощью метода Node: getWorldDirection ():
Unreal Engine 4 | UNIGINE |
|
|
См. также:
Более плавный игровой процесс с DeltaTime / IFps
В Unreal Engine 4, чтобы гарантировать, что определенные действия выполняются за одно и то же время независимо от частоты кадров (например, изменение положения один раз в секунду и т. д.), используется множитель deltaTime (время в секундах, которое потребовалось для завершения последнего кадра), передаваемый методу Tick (float deltaTime). То же самое в UNIGINE называется Game: getIFps ():
Unreal Engine 4 | UNIGINE |
|
|
Рисование отладочных данных
Unreal Engine 4:
DrawDebugLine(GetWorld(), traceStart, traceEnd, FColor::Green, true, 1.0f);
В UNIGINE за вспомогательную отрисовку отвечает синглтон Visualizer:
// включаем вспомогательную визуализацию
Visualizer::setEnabled(true);
/*..*/
Visualizer::renderLine3D(vec3_zero, vec3(5, 0, 0), vec4_one);
Visualizer::renderVector(node->getPosition(), node->getDirection(Math::AXIS_Y) * 10, vec4(1, 0, 0, 1));
Примечание. Visualizer также можно включить с помощью консольной команды show_visualizer 1.
См. также:
Поиск Actor / Node
Unreal Engine 4:
// поиск Actor или UObject по имени
AActor* MyActor = FindObject(nullptr, TEXT("MyNamedActor"));
// Поиск Actor по типу
for (TActorIterator It(GetWorld()); It; ++It)
{
AMyActor* MyActor = *It;
// ...
}
UNIGINE:
// поиск Node по имени
NodePtr my_node = World::getNodeByName("my_node");
// поиск всех нод с данным именем
Vector nodes;
World::getNodesByName("test", nodes);
// получение прямого потомка ноды
int index = node->findChild("child_node");
NodePtr direct_child = node->getChild(index);
// Рекурсивный поиск ноды по имени среди всех потомков в иерархии
NodePtr child = node->findNode("child_node", 1);
Приведение от типа к типу
Классы всех типов нод являются производными от Node в UNIGINE, поэтому чтобы получить доступ к функциональности ноды определенного типа (например, ObjectMeshStatic), необходимо провести понижающее приведение типа — Downcasting (приведение от базового типа к производному), которое выполняется с использованием специальных конструкций. Чтобы выполнить Upcasting (приведение от производного типа к базовому), можно как обычно просто использовать сам экземпляр:
Unreal Engine 4 | UNIGINE |
|
|
Уничтожение Actor / Node
Unreal Engine 4 | UNIGINE |
|
|
Для выполнения отложенного удаления ноды в UNIGINE можно создать компонент, который будет отвечать за таймер и удаление.
Создание экземпляра Actor / Node Reference
За создание нового экземпляра actor (Spawning) отвечает метод UWorld: SpawnActor ():
AKAsset* SpawnedActor1 = (AKAsset*)
GetWorld()->SpawnActor(AKAsset::StaticClass(), NAME_None, &Location);
В Unreal Engine 4 клонировать существующий actor можно следующим образом:
AMyActor* CreateCloneOfMyActor(AMyActor* ExistingActor, FVector SpawnLocation, FRotator SpawnRotation)
{
UWorld* World = ExistingActor->GetWorld();
FActorSpawnParameters SpawnParams;
SpawnParams.Template = ExistingActor;
World->SpawnActor(ExistingActor->GetClass(), SpawnLocation, SpawnRotation, SpawnParams);
}
В UNIGINE используйте Node: clone () для клонирования ноды, существующей в мире, и World: loadNode для загрузки иерархии нод из ассета .node. В этом случае на сцену будет добавлена вся иерархия нод, которая была сохранена как Node Reference. Вы можете обратиться к ассету либо через параметр компонента, либо вручную, указав виртуальный путь к нему:
// MyComponent.h
PROP_PARAM(File, node_to_spawn);
// MyComponent.cpp
/* .. */
void MyComponent::init()
{
// создание новой ноды Dummy
NodeDummyPtr dummy = NodeDummy::create();
// клонирование существующей ноды
NodePtr cloned = dummy->clone();
// загрузка иерархии нод из ассета
NodePtr spawned = World::loadNode(node_to_spawn.get());
spawned->setWorldPosition(node->getWorldPosition());
// загрузка с указанием пути в файловой системе
NodePtr spawned_manually = World::loadNode("nodes/node_reference.node");
}
Для параметра компонента также необходимо указать ассет .node в редакторе:
Еще один способ загрузить содержимое ассета *.node — создать NodeReference и работать с иерархией нод как с одним объектом. Тип Node Reference имеет ряд внутренних оптимизаций и тонких моментов (кэширование нод, распаковка иерархии и т.д.), поэтому важно учитывать специфику работы с этими объектами.
void MyComponent::update()
{
NodeReferencePtr nodeRef = NodeReference::create("nodes/node_reference_0.node");
}
Запуск скриптов в редакторе
Unreal Engine 4 позволяет расширять функциональность редактора с помощью Blueprint/Python скриптов.
UNIGINE не поддерживает выполнение логики приложения на C++ внутри редактора. Основной способ расширить функциональность редактора — плагины, написанные на C++.
Для быстрого тестирования или автоматизации разработки можно написать логику на UnigineScript. UnigineScript API обладает только базовой функциональностью и ограниченной сферой применения, но доступен для любого проекта на UNIGINE, включая проекты на C++.
Есть два способа добавить скриптовую логику в проект:
Создайте ассет скрипта .usc.
Определите в нем логику. При необходимости добавьте проверку, загружен ли редактор:
//Исходный код (UnigineScript)
#include
vec3 lookAtPoint = vec3_zero;
Node node;
int init() {
node = engine.world.getNodeByName("material_ball");
return 1;
}
int update() {
if(engine.editor.isLoaded())
node.worldLookAt(lookAtPoint);
return 1;
}
Выделите текущий мир и укажите для него сценарий мира. Нажмите Apply и перезагрузите мир.
Проверьте окно консоли на наличие ошибок.
После этого логика скрипта будет выполняться как в редакторе, так и в приложении.
Используя WorldExpression. С той же целью можно использовать ноду WorldExpression, выполняющую логику при добавлении в мир:
Нажмите Create → Logic → Expression и поместите новую ноду WorldExpression в мир.
Напишите логику на UnigineScript в поле Source:
//Исходный код (UnigineScript)
{
vec3 lookAtPoint = vec3_zero;
Node node = engine.world.getNodeByName("my_node");
node.worldLookAt(lookAtPoint);
}
Проверьте окно Console на наличие ошибок.
Логика будет выполнена немедленно.
Триггеры
Unreal Engine 4:
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
// компонент триггера
UPROPERTY()
UPrimitiveComponent* Trigger;
AMyActor()
{
Trigger = CreateDefaultSubobject(TEXT("TriggerCollider"));
Trigger.bGenerateOverlapEvents = true;
Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly);
}
virtual void NotifyActorBeginOverlap(AActor* Other) override;
virtual void NotifyActorEndOverlap(AActor* Other) override;
};
В UNIGINE Trigger — это специальный тип нод, вызывающих события в определенных ситуациях:
Важно! PhysicalTrigger не обрабатывает события столкновения, для этого тела и сочленения предоставляют свои собственные события.
WorldTriger — наиболее распространенный тип триггера, который можно использовать в игровой логике:
WorldTriggerPtr trigger;
int enter_callback_id;
// коллбэк при попадании внутрь объема триггера
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() {
// создание WorldTrigger ноды
trigger = WorldTrigger::create(Math::vec3(3.0f));
// подписка на событие попадания ноды внутрь объема триггера
// и сохранение id коллбэка для будущего удаления
enter_callback_id = trigger->addEnterCallback(MakeCallback(this, &AppWorldLogic::enter_callback));
// подписка на событие покидания нодой объема триггера
trigger->addLeaveCallback(MakeCallback(this, &AppWorldLogic::leave_callback));
return 1;
}
Обработка ввода
Unreal Engine 4:
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:
/* .. */
#include
#include
#include
/* .. */
void MyInputController::update()
{
// при нажатии правой кнопки мыши
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
Math::ivec2 mouse = Input::getMouseCoord();
// сообщить координаты курсора мыши в консоль
Log::message("Right mouse button was clicked at (%d, %d)\n", mouse.x, mouse.y);
}
// закрыть приложение при нажатии клавиши 'Q' с учетом того, открыта ли консоль
if (Input::isKeyDown(Input::KEY_Q) && !Console::isActive())
{
App::exit();
}
}
/* .. */
Также можно использовать синглтон ControlsApp для обработки привязок элементов управления к набору предустановленных состояний ввода. Чтобы настроить привязки, откройте настройки Controls в редакторе:
#include
/* .. */
void MyInputController::init()
{
// переназначение состояний клавишам и кнопкам вручную
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");
}
}
/* .. */
Проверка пересечения луча с геометрией (Raycast)
Unreal Engine 4:
APawn* AMyPlayerController::FindPawnCameraIsLookingAt()
{
FCollisionQueryParams Params;
Params.AddIgnoredActor(GetPawn());
FHitResult Hit;
FVector Start = PlayerCameraManager->GetCameraLocation();
FVector End = Start + (PlayerCameraManager->GetCameraRotation().Vector() * 1000.0f);
bool bHit = GetWorld()->LineTraceSingle(Hit, Start, End, ECC_Pawn, Params);
if (bHit)
{
return Cast(Hit.Actor.Get());
}
return nullptr;
}
В UNIGINE то же самое достигается с помощью Intersections:
#include "MyComponent.h"
#include
#include
#include
#include
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;
// игнорируем поверхности мешей с включенными битами маски Intersection
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);
}
}
Напоминаем, что получить доступ к бесплатной версии UNIGINE 2 Community можно, заполнив форму на нашем сайте.
Все комплектации UNIGINE:
Community — базовая версия для любителей и независимых разработчиков. Достаточна для разработки видеоигр большинства популярных жанров (включая VR).
Engineering — расширенная, специализированная версия. Включает множество заготовок для инженерных задач.
Sim — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.
Подробнее о комплектациях и ценах