Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Написание игровой логики, триггеры, ввод, рейкастинг и другое.
Специально для тех, кто ищет альтернативу 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: String str("Hello, UNIGINE 2!"); |
Контейнеры | TArray, TMap, TSet | Vector, Map, Set и другие: 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 — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.
Подробнее о комплектациях и ценах