Написание игровой логики, триггеры, ввод, рейкастинг и другое.

Специально для тех, кто ищет альтернативу 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:
-
Visual Studio 2015+;
-
CMake;
-
Qt-based: Qt Creator, QMake или CMake (доступно для Engineering и Sim редакций SDK);
-
-
Linux:
-
GNU Make.
-
Далее просто выберите 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 <UnigineComponentSystem.h> /* .. */ int AppSystemLogic::init() { Unigine::ComponentSystem::get()->initialize(); return 1; }
И тогда можно написать новый компонент:
MyComponent.h:
#pragma once #include <Unigine.h> #include <UnigineComponentSystem.h> 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 ноде.
-
Наконец, работу логики компонента можно проверить, запустив приложение.
Чтобы узнать больше о последовательности выполнения и о том, как создавать компоненты, перейдите по ссылкам ниже:
-
Использование C++ Component System.
Немного про API
Все объекты в Unreal Engine 4 наследуются от UObject, доступ к ним возможен при помощи стандартных C++ указателей или умных указателей Unreal Smart Pointer Library.
В UNIGINE API есть система умных указателей, управляющих существованием нод и других объектов в памяти:
// создать ноду типа NodeType <NodeType>Ptr nodename = <NodeType>::create(<construction_parameters>); // удалить ноду из мира 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<MyComponent>(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 str(«Hello, UNIGINE 2!»); |
|
Контейнеры |
TArray, TMap, TSet |
Vector<NodePtr> nodes; World::getNodes(nodes); for(NodePtr n : nodes) { } |
|
Векторы и матрицы |
FVector3f — FVector3d, FMatrix44f — FMatrtix44d и другие |
vec3 — dvec3, mat4 — dmat4 и другие типы в математической библиотеке. |
UNIGINE поддерживает как одинарную точность (Float), так и двойную точность координат (Double), доступную в зависимости от редакции SDK. Почитайте про использование универсальных типов данных, подходящих под любой проект.
Основные примеры кода
Вывод в консоль
|
Unreal Engine 4 |
UNIGINE |
|
|
|
См. также:
-
Дополнительные типы сообщений в API класса Log.
Загрузка сцены
|
Unreal Engine 4 |
UNIGINE |
|
|
|
Доступ к Actor / Node из компонента
|
Unreal Engine 4 |
UNIGINE |
|
|
N |
См. также:
-
Видеоруководство, демонстрирующее, как получить доступ к нодам из компонентов с помощью C++ Component System.
Доступ к компоненту из Actor / Node
Unreal Engine 4:
UMyComponent* MyComp = MyActor->FindComponentByClass<UMyComponent>();
UNIGINE:
MyComponent *my_component = getComponent<MyComponent>(node);
Работа с направлениями
В Unreal Engine 4 компонент USceneComponent (или производный) отвечает за действия с трансформацией actor’а. Чтобы получить вектор направления по одной из осей с учетом ориентации в мировых координатах, можно использовать соответствующие методы USceneComponent (GetForwardVector()) или AActor (GetActorForwardVector()).
В UNIGINE трансформация ноды в пространстве представлена ее матрицей трансформации (mat4), а все основные операции с трансформацией или иерархией нод доступны при помощи методов класса Node. Такой же вектор направления в UNIGINE получается с помощью метода Node::getWorldDirection():
|
Unreal Engine 4 |
UNIGINE |
|
|
|
См. также:
-
Система координат в 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.
См. также:
-
Все типы визуализаций в API класса Visualizer.
Поиск Actor / Node
Unreal Engine 4:
// поиск Actor или UObject по имени AActor* MyActor = FindObject<AActor>(nullptr, TEXT("MyNamedActor")); // Поиск Actor по типу for (TActorIterator<AMyActor> It(GetWorld()); It; ++It) { AMyActor* MyActor = *It; // ... }
UNIGINE:
// поиск Node по имени NodePtr my_node = World::getNodeByName("my_node"); // поиск всех нод с данным именем Vector<NodePtr> 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<AMyActor>(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 <core/unigine.h> 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<USphereComponent>(TEXT("TriggerCollider")); Trigger.bGenerateOverlapEvents = true; Trigger.SetCollisionEnabled(ECollisionEnabled::QueryOnly); } virtual void NotifyActorBeginOverlap(AActor* Other) override; virtual void NotifyActorEndOverlap(AActor* Other) override; };
В UNIGINE Trigger — это специальный тип нод, вызывающих события в определенных ситуациях:
-
NodeTrigger вызывает коллбэк при изменении состояния включен/выключен и позиции самой ноды.
-
WorldTrigger вызывает коллбэк, когда какая-либо нода (независимо от типа) попадает внутрь или за его пределы.
-
PhysicalTrigger вызывает коллбэк, когда физические объекты попадают внутрь или за его пределы.
Важно! 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 <UnigineApp.h> #include <UnigineConsole.h> #include <UnigineInput.h> /* .. */ 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 <Unigine.h> /* .. */ 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<APawn>(Hit.Actor.Get()); } return nullptr; }
В UNIGINE то же самое достигается с помощью Intersections:
#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; // игнорируем поверхности мешей с включенными битами маски 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 — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.
ссылка на оригинал статьи https://habr.com/ru/company/unigine/blog/670518/
Добавить комментарий