Основа геймплея для Unreal Engine 4 предоставляет разработчику мощный набор классов для создания игры. Ваш проект может быть шутером, симулятором фермы, глубокой RPG — это неважно, основа очень универсальна, делает за вас часть тяжёлой работы и задаёт некоторые стандарты. Она довольно сильно интегрирована в движок, поэтому рекомендую вам придерживаться этих классов, а не пытаться изобретать собственную основу игры, как это часто бывает в движках наподобие Unity3D. Понимание этой основы очень важно для успешной и эффективной работы над проектами.
Для кого эта статья?
Для всех, кого интересует создание игр в UE4, а конкретно на C++, и кто хочет больше узнать о основе геймплея Unreal. В этом посте рассматриваются базовые классы, которые вы будете использовать в основе геймплея, и объясняется их применение, процесс создания их экземпляров движком и способ получения доступа к этим классам из других частей кода игры. Бо?льшая часть информации справедлива также и для блюпринтов.
Если вы хотите познакомиться с основами Unreal Engine 4, то изучите моё предыдущее руководство. Также у меня есть отдельное руководство, посвящённое виртуальной реальности для начинающих. Оно пригодится тем, кто изучает специфику VR в Unreal Engine 4.
При создании игр в Unreal Engine 4 вы встретите много уже готовых boilerplate-заготовок. Существует несколько классов, которые вы часто будете использовать при создании игр на C++ или в блюпринтах. Мы рассмотрим каждый из этих классов, их приятные особенности и узнаем, как ссылаться на них из других частей кода. Бо?льшая часть информации этого руководства применима и к блюпринтам, однако я использую фрагменты кода на C++ и поэтому некоторые функции будут недоступны в блюпринтах и полезны только для пользователей C++.
Actor
Наверно, самый часто используемый класс в играх. Actor — это основа для любого объекта на уровне, в том числе для игроков, управляемых ИИ врагов, дверей, стен и геймплейных объектов. Акторы создаются с помощью таких ActorComponents (см. следующий раздел), как StaticMeshComponent, CharacterMovementComponent, ParticleComponent и многих других. Даже такие классы, как GameMode (см. ниже) являются акторами (хотя у GameMode нет «реального» положения в мире). Давайте обсудим пару аспектов, которые вам нужно знать об акторах.
Actor — это класс, который можно реплицировать по сети (для многопользовательского режима). Это легко делается с помощью вызова в конструкторе SetReplicates(true). Для создания эффективного сетевого программирования акторов необходимо учитывать множество аспектов, которые я не смогу рассмотреть в этой статье.
Акторы поддерживают концепцию получения урона. Урон может наноситься непосредственно актору с помощью MyActor->TakeDamage(...) или через UGameplayStatics::ApplyDamage(...). Стоит учесть, что существуют вариации: PointDamage (например, для оружия, попадание из которого вычисляется трассировкой луча (hitscan)) и RadialDamage (например, для взрывов). На официальном сайте Unreal Engine есть замечательная вводная статья Damage in UE4.
Создать новый экземпляр актора в коде можно просто с помощью GetWorld()->SpawnActor<T>(…); где T — это возвращаемый класс, например, AActor для одного из ваших собственных классов — AGadgetActor, AGameplayProp и т.д.
Вот пример кода, в котором актор создаётся в процессе выполнения приложения:
FTransform SpawnTM;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Owner = GetOwner();
/* Attempt to assign an instigator (used for damage application) */
SpawnParams.Instigator = Cast<APawn>(GetOwner());
ASEquippableActor* NewItem = GetWorld()->SpawnActor<ASEquippableActor>(NewItemClass, SpawnTM, SpawnParams);
Существует множество способов получения доступа к акторам. Обычно у вас будет указатель/ссылка на конкретный актор, который вам нужен. В показанном выше примере мы сохраняем указатель на актор надеваемого предмета в переменной и начинаем через неё манипулировать экземпляром актора.
Очень полезной функцией, которую можно использовать при прототипировании или освоении движка, является UGameplayStatics::GetAllActorsOfClass(…). Она позволяет нам получит массив из всех акторов передаваемого класса (в том числе и порождённых классов; если передать в качестве класса Actor, то мы получим ВСЕ объекты уровня). Этой функции часто боятся и избегают как не очень эффективного способа взаимодействия с окружением, но иногда это единственный доступный инструмент.
Акторы не имеют собственных переноса, поворота или масштаба. Всё это задаётся и получается с помощью RootComponent, т.е. компонента верхнего уровня в иерархии SceneComponents (подробнее о SceneComponents рассказывается ниже). Наиболее часто используемые функции наподобие MyActor->GetActorLocation() на самом деле переходят к RootComponent и возвращают его расположение в мире.
Вот ещё несколько полезных функций, которые используются в контексте актора:
- BeginPlay // «Первая» функция, вызываемая после создания и полной инициализации актора. Это удобное место для задания базовой логики, таймера и внесения изменений в свойства, потому что актор уже полностью инициализирован и может выполнять запросы к своему окружению.
- Tick // Вызывается в каждом кадре. Для большинства акторов можно отключить её из соображений производительности, но по умолчанию она включена. Замечательно подходит для быстрой настройки динамической логики и проверки условий в каждом кадре. Постепенно вы начнёте перемещать всё больше кода связанной с событиями логики из таймеров в логику, работающую на меньших частотах.
- EndPlay // Вызывается, когда актор удаляется из мира. Содержит «EEndPlayReason», в котором указывается причина вызова.
- GetComponentByClass // Находит один экземпляр компонента определённого класса. Очень полезно, когда вы не знаете точного типа актора, но знаете, что он должен содержать определённый тип компонента. Также существует GetComponentsByClass, возвращающая все экземпляры класса, а не только первый найденный.
- GetActorLocation // И все его вариации — *Rotation, *Scale, в том числе и SetActorLocation и т.д.
- NotifyActorBeginOverlap // Удобна для проверки наложений, вызванных любым из его компонентов. Таким способом можно быстро настраивать геймплейные триггеры.
- GetOverlappingActors // Находит, какие другие акторы пересекаются с выбранным. Существует также вариант для компонентов: GetOverlappingComponents
Actor содержит огромный функционал и множество переменных — он является фундаментом основы геймплея в Unreal Engine, поэтому это не удивительно. Для дальнейшего исследования этого класса неплохо будет открыть файл заголовка Actor.h в Visual Studio и посмотреть, какой функционал в нём есть. В статье же нам предстоит ещё многое рассмотреть, поэтому давайте перейдём к следующему классу в списке.
ActorComponent
Компоненты располагаются внутри акторов, стандартными компонентами считаются StaticMeshComponent, CharacterMovementComponent, CameraComponent и SphereComponent. Каждый из этих компонентов обрабатывает свою частную задачу, например, движение, физическое взаимодействие (например, объём коллизии для чёткой проверки взаимодействующих акторов) или визуально отображает что-то в мире, например, меш игрока.
Подклассом этого компонента является SceneComponent — это базовый класс для всего, связанного с Transform (Position, Rotation, Scale), поддерживающий прикрепление. Например, мы можем прикрепить CameraComponent к SpringArmComponent для настройки камеры от третьего лица. Для правильной настройки относительного расположения требуются и transform, и прикрепление.
Чаще всего компоненты создаются в конструкторе актора, но также можно создавать и уничтожать их в процессе выполнения. Для начала давайте рассмотрим один из конструкторов моего актора.
ASEquippableActor::ASEquippableActor()
{
PrimaryActorTick.bCanEverTick = true;
MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("MeshComp"));
MeshComp->SetCollisionObjectType(ECC_WorldDynamic); // Just setting a property on our component
RootComponent = MeshComp; // Set our root component for other SceneComponents to attach to
ItemComp = CreateDefaultSubobject<USItemComponent>(TEXT("ItemComp")); // not attached to anything (not a SceneComponent with a transform)
}
USkeletalMeshComponent создаётся с помощью CreateDefaultSubobject<T> (функции актора) и требует указания имени (это имя можно увидеть в списке компонентов блюпринта). Если вы пишете код игры на C++, то часто будете использовать эту функцию, но ТОЛЬКО внутри контекста конструктора.
Можно также заметить, что мы задаём MeshComp в качестве нового RootComponent. Теперь все Scene Component должны прикрепляться к этому мешу, что можно легко сделать с помощью следующей строки:
WidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("InteractWidgetComp"));
WidgetComp->SetupAttachment(MeshComp);
SetupAttachment займётся обработкой исходного прикрепления; ожидается, что он будет вызываться в конструкторе для ВСЕХ компонентов сцены, кроме самого RootComponent. Можно задаться вопросом, почему мой ItemComponent не вызывает эту функцию SetupAttachment. Так получилось просто потому, что этот компонент является ActorComponent, но НЕ SceneComponent и не имеет Transform (позиции, поворота, масштаба), а потому не должен добавляться в иерархию. Тем не менее, компонент всё равно будет регистироваться с Actor. То, что он отделён от иерархии, означает, что функции наподобие MyActor->GetComponentByClass будут возвращать все ActorComponents и SceneComponents.
Наряду с Actor, эти компоненты критически важны для создания игры как на C++, так и на блюпринтах. Именно они являются строительными кирпичиками игры. Вы запросто можете создать собственные компоненты, чтобы они обрабатывали какие-то специфические аспекты игры, например HealthComponent, хранящий очки здоровья и реагирующий на урон, получаемый его родительским актором.
С помощью представленного ниже кода можно создавать в процессе выполнения собственные компоненты. Это отличается от поведения CreateDefaultSubobject, используемого только для конструкторов.
UActorComponent* SpawnedComponent = NewObject<UActorComponent>(this, UStaticMeshComponent::StaticClass(), TEXT("DynamicSpawnedMeshCompoent"));
if (SpawnedComponent)
{
SpawnedComponent->RegisterComponent();
}
Вот часть полезного функционала ActorComponents:
- TickComponent() // Как и Tick() актора, выполняется каждый кадр для обработки высокочастотной логики.
- bool bIsActive и связанные с ней функции наподобие Activate, Deactivate,… Используются для полного включения/отключения компонента (в том числе и TickComponent) без уничтожения компонента и удаления его из актора.
Чтобы обеспечить репликацию ActorComponent, необходимо вызвать функцию SetIsReplicated(true), название которой слегка отличается от функции актора. Это необходимо только тогда, когда вам нужно реплицировать конкретную часть логики компонента, например переменную при вызовах функции, то есть реплицировать нужно не все компоненты реплицируемого актора.
PlayerController
Это базовый класс для игрока, получающего ввод от пользователя. Сам по себе PlayerController не отображается визуально в окружении, вместо этого он управляет экземпляром Pawn, определяющим визуальное и физическое представление этого игрока в мире. Во время игрового процесса игрок может обладать несколькими разными Pawn (например, транспортным средством или свежей копией Pawn при респауне), а экземпляр PlayerController остаётся одинаковым на протяжении всего уровня. Это важно, потому что в некоторые моменты PlayerController может не обладать вообще никакими Pawn. Это значит, что такие вещи, как открытие меню должны добавляться к PlayerController, а не к классу Pawn.
В многопользовательских играх PlayerController существует только на владеющем им клиенте и на сервере. Это значит, что в игре на 4 игрока у сервера есть 4 контроллера игроков, а у каждого клиента — только по одному. Это очень важно понимать, когда необходимо использовать переменные; если для всех игроков требуется репликация переменной игрока, то она должна существовать не в PlayerController, а в Pawn или даже в PlayerState (рассмотренном ниже).
Получение доступа к PlayerControllers
- GetWorld()->GetPlayerControllerIterator() // GetWorld доступен в любом экземпляре Actor
- PlayerState->GetOwner() // владелец playerstate имеет тип PlayerController, и вы должны передавать его PlayerController самостоятельно.
- Pawn->GetController() // Задаётся только тогда, когда пауном уже владеет (т.е. управляет) PlayerController.
Этот класс содержит PlayerCameraManager, который обрабатывает view targets и transforms камеры, в том числе и тряску. Ещё одним важным классом, которым управляет PlayerController, является HUD (рассмотрен ниже). Он применяется для рендеринга на Canvas (теперь используется не так часто, потому что есть UMG) и его можно использовать для управления данными, которые необходимо передать в интерфейс UMG.
Когда к GameMode подключается новый игрок, для этого игрока в классе GameModeBase с помощью Login() создаётся PlayerController.
Pawn
Является физическим и визуальным представлением того, чем управляет игрок (или ИИ). Это может быть машина, воин, башня или что угодно, обозначающее персонажа игры. Стандартным подклассом Pawn является Character, в котором реализуется SkeletalMesh и, что более важно, CharacterMovementComponent со множеством опций для точной настройки движения игрока по окружению с помощью обычного шутерного движения.
В многопользовательских играх каждый экземпляр Pawn реплицируется другим клиентам. Это значит, что в игре на 4 игрока и на сервере, и на каждом клиенте есть 4 экземпляра pawn. Довольно часто экземпляр Pawn «убивают» при смерти игрока, а при респауне создаётся новый экземпляр. Имейте это в виду при хранении данных, которые должны сохраняться после завершения жизни игрока (или полностью откажитесь от этого паттерна и постоянно оставляйте экземпляр pawn живым)
Получение доступа к Pawn
- PlayerController->GetPawn() // Только когда PlayerController владеет Pawn
- GetWorld()->GetPawnIterator() // GetWorld доступен для любого экземпляра Actor и возвращает ВСЕ Pawn, в том числе и для ИИ.
Создание
GameModeBase создаёт Pawn с помощью SpawnDefaultPawnAtTransform. Класс GameModeBase также определяет, какой класс Pawn нужно создавать.
GameModeBase
Базовый класс, определяющий используемые классы (PlayerController, Pawn, HUD, GameState, PlayerState). Часто используется для задания правил игры в таких режимах, как «Capture the Flag»; он может обрабатывать флаги или волны врагов. Обрабатывает и другие важные функции, такие как создание игрока.
GameMode — это подкласс GameModeBase. Он содержит ещё несколько функций, которые изначально использовались в Unreal Tournament, такие как MatchState и другие шутерные функции.
В многопользовательском режиме класс GameMode существует только на сервере! Это значит, что ни у одного клиента нет его экземпляра. В однопользовательских играх он не имеет никакого влияния. Для реплицирования функций и хранения данных, необходимых для GameMode, можно использовать GameState, существующий на всех клиентах и созданный специально для этой цели.
Получение доступа к GameMode
- GetWorld()->GetAuthGameMode() // GetWorld доступен для любого экземпляра Actor.
- GetGameState() // возвращает gamestate для репликации функций и/или переменных
- InitGame(…) // инициализирует некоторые из правил игры, в том числе, указанные в URL (например, «MyMap?MaxPlayersPerTeam=2»), которые можно передавать при загрузке уровней в игре.
HUD
Это класс интерфейса пользователя. В нём содержится много кода Canvas, который является кодом отрисовки интерфейса пользователя, написанного до появления UMG. Сегодня основной работой по отрисовке интерфейса пользователя занимается UMG.
Класс существует только в клиенте. Репликация невозможна. Им владеет PlayerController.
Получение доступа к HUD
PlayerController->GetHUD() // Доступен в локальном PlayerController.
Создание
Создаётся с помощью SpawnDefaultHUD (создаёт обычный AHUD) внутри PlayerController, который владеет HUD, а затем переопределяется GameModeBase с помощью InitializeHUDForPlayer классом HUD, указанным в GameModeBase.
Мои личные примечания
Я всё реже и реже стал пользоваться этим классом, и использую UMG, которым можно управлять через PlayerController. Не забывайте — перед созданием виджетов в многопользовательских играх нужно убедиться, что контроллер игрока является IsLocalController().
World
UWorld — это объект верхнего уровня, представляющий карту, на которой будут существовать и рендериться акторы и компоненты. Содержит постоянный уровень и многие другие объекты, такие как gamestate, gamemode, а также списки находящихся на карте Pawns и Controllers.
Трассировка линий и все её вариации выполняются через World с помощью таких функций, как World->LineTraceSingleByChannel и многих других подобных вариаций.
Получение доступа к World
Для получения доступа достаточно вызвать GetWorld() внутри акторов.
Когда необходимо получить экземпляр World в статических функциях, то нужно передавать WorldContextObject, по сути являющийся словом для любого актора, которого можно использовать для вызова ->GetWorld(). Вот пример из одного моего файла заголовка:
static APlayerController* GetFirstLocalPlayerController(UObject* WorldContextObject);
GameInstance
GameInstance имеет один экземпляр, который продолжает существовать на протяжении длительности всей игры. При переходах между картами и меню будет сохраняться один и тот же экземпляр этого класса. Этот класс можно использовать для создания обработчиков событий или обработки сетевых ошибок, загрузки таких пользовательских данных, как параметры игры и функций, которые относятся не только к одному уровню игры.
Получение доступа к GameInstance
- GetWorld()->GetGameInstance<T>(); // где T — тип класса, например GetGameInstance<UGameInstance>() или вас собственный порождённый тип.
- Actor->GetGameInstance()
Мои личные примечания
Обычно не используется на ранних этапах проекта. Не делает ничего критически важного, если только вы не углубитесь в разработку (может управлять такими аспектами, как игровые сессии, воспроизведение демо или передача данными между уровнями)
PlayerState
Контейнер для переменных, реплицируемых между клиентом/сервером для отдельного игрока. В многопользовательских играх он не предназначен для выполнения логики и является просто контейнером данных, поскольку PlayerController недоступен для всех клиентов, а Pawn часто уничтожается при смерти игрoка, поэтому неприменим для данных, которые должны храниться после смерти.
Получение доступа к PlayerState
Pawn содержит его как переменную Pawn->PlayerState, также доступную в Controller->PlayerState. PlayerState в Pawn назначен только тогда, когда Pawn владеет Controller, в противном случае имеет значение nullptr.
Список всех имеющихся экземпляров PlayerState (например, всех игроков, находящихся в матче) можно получить через GameState->PlayerArray.
Создание
Создающий класс назначается в GameMode (PlayerStateClass) и создаётся в AController::InitPlayerState()
Мои личные примечания
Полезен только при работе над многопользовательскими играми.
GameStateBase
Похож на PlayerState, но предоставляет клиентам информацию о GameMode. Так как экземпляр GameMode существует не в клиентах, а только на сервере, этот класс является полезным контейнером для репликации информации, например, о завершении времени матча, очков команды и т.д.
Имеет две вариации — GameState и GameStateBase. GameState обрабатывает дополнительные переменные, требуемые GameMode (в отличие от GameModeBase)
Получение доступа к GameStateBase
- World->GetGameState<T>() // где T — вызываемый класс, например GetGameState<AGameState>()
- MyGameMode->GetGameState() // хранится и доступен в экземпляре gamemode (необходим только на сервере, который владеет единственным экземпляром GameMode); клиенты должны использовать указанный выше вызов.
Мои личные примечания
Используйте GameStateBase вместо GameState, только если gamemode не наследуется от GameMode вместо GameModeBase.
UObject
Базовый объект практически для всего в движке. Из UObject наследуются акторы, а также другие базовые классы, такие как GameInstance. Он никогда не должен использоваться для рендеринга, но очень полезен для хранения данных и функции, когда под ваши требования не подходят struct.
Создание UObjects
UObjects не спаунятся подобно акторам, а создаются с помощью NewObject<T>(). Например:
TSubclassOf<UObject> ClassToCreate;
UObject* NewDesc = NewObject<UObject>(this, ClassToCreate);
Личные примечания
Маловероятно, что вы будете создавать классы непосредственно из UObject, если только вы не хорошо освоили движок и не хотите углубиться в создание собственных систем. Я, например, использую его для хранения списков информации из базы данных.
Его можно использовать для сети, но требуется дополнительная настройка класса объектов, а объекты должны храниться в акторе.
GameplayStatics
Статические классы используются для обработки различного стандартного функционала игр, например, воспроизведения звуков и создания эффектов частиц, создания акторов, применения урона к акторам, получения Pawn игрока, PlayerController и т.д. Этот класс очень полезен для всевозможного доступа к геймплейным функциям. Все функции являются статическими, то есть вам не требуется указатель на экземпляр этого класса и вы можете вызывать функции напрямую из любого места, как это показано в примере ниже.
Получение доступа к GameplayStatics
Так как GameplayStatics является UBlueprintFunctionLibrary, вы можете получить к нему доступ из любого места кода (или блюпринта)
UGameplayStatics::WhateverFunction(); // static functions are easily accessed anywhere, just include #include "Kismet/GameplayStatics.h"
Мои личные примечания
В этом классе множество полезных функций и его обязательно нужно знать при создании любой игры. Рекомендую изучить его, чтобы узнать о его возможностях.
Ссылки
Рекомендуемые для изучения основ геймплея и программирования в Unreal Engine 4 материалы.
Комментарии (3)
SinsI
11.05.2018 21:59Перевод — просто ужас!
Оригинальное название правильно переводится примерно как «Введение в Фреймворк Unreal Gameplay Engine».
«Основа геймплея для игры на С++ для Unreal Engine» — это перемещаться в трёхмерном лабиринте, стреляя в монстров и подбирая ресурсы, и текст статьи не имеет к этому ни малейшего отношения.
BingoBongo
Я бы добавил GEngine, UGameViewportClient, custom UGameViewportClient и методы для дебага, типа AddOnScreenDebugMessage() и DrawDebugLine(). И еще то что в LineTraceSingleByChannel() можно передать TraceTag, а в World указать DebugDrawTraceTag, чтобы на экране отображалось что, где и как пересеклось. Не знаю, может это тема следующего поста, но про основы пишут все, а про элементарные тонкости никто не рассказывает.