Ввод в Unreal Engine
Небольшой гайд по созданию ввода в UE с нуля, как в С++ кода так и в Blueprints (далее для краткости BP), старая и новая (EnhancedInput) системы ввода. Пишу больше как заметку для себя.
Создание проекта и подготовка окружения
Создадим пустой проект на С++, с максимальным качеством и без стартер контента, назову ForAll
После создания проекта в корне с проектом создается файл `.vsconfig` представляющий собой конфигурацию установщика студии, у меня его содержимое вот такое
| JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
| {
"version": "1.0",
"components": [
"Microsoft.Net.Component.4.6.2.TargetingPack",
"Microsoft.VisualStudio.Component.VC.14.38.17.8.x86.x64",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"Microsoft.VisualStudio.Component.Windows10SDK.22621",
"Microsoft.VisualStudio.Workload.CoreEditor",
"Microsoft.VisualStudio.Workload.ManagedDesktop",
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.NativeGame"
]
} |
|
Сейчас самая свежая версия студии это 17.10.3, а в этом файле указан компилятор версии 17.8, движок версии 5.4.2 работает и с 17.10, но при сборке выдает предупреждение 11>EXEC: Warning : Visual Studio 2022 compiler is not a preferred version, На сколько помню версия 5.3 была несовместима с версией 17.9, в 5.4 могло остаться то же самое.
После создания откроется главное окно редактора. Проект плюсовый и должен быть каталог с с++ классами, но его нету, возможно баг, но не важно.
Создадим новый уровень File -> New Level, выберем пресет Empty level
Окно вьюпорта изменится, оно станет черным

В папке Content создадим папку Input и в нее сохраним наш уровень (Ctrl + S) под именем InputMap
Теперь создадим GameMode в той же папке, для этого правой кнопкой мыши и создать Blueprint class и в списке выбираем GameModeBase. Теперь включим вкладку настроек мира через Window -> World Settings и в GameMode Override выберем наш созданным GameMode.
Теперь добавим стандартное небо, для этого откроем окно Env. Light Mixer с помощью Window -> Env. Light Mixer

В котором сверху поочередно нажмем все имеющиеся кнопки Create Sky Light -> Create Atmospheric Fog -> Create Sky Atmosphere -> Create Volumetric Cloud -> Create Height Fog. После этого закроем окно и увидим во вьюпорте небо.

Добавим на уровень поверхность где будем ходить в виде простого кубика с измененным масштабом.
Масштаб настроим так 10, 10 и 0.1 по осям X, Y и Z. И последним шагом добавим Player Start.
Поместим его в центр нашей платформы установив координаты в 0, потом поднимем над поверхностью и нажмем End, объект опустится на поверхность.

Теперь настроим чтобы при запуске редактора и упаковке игры открывалась наша карта, для этого по пути Edit -> Project Settings -> Maps & Modes выбираем наш уровень в Editor Startup Map и Game Default Map
Старое управление
В последних версиях движка оно устарело (deprecated) и не желательно к использованию. Из плюсов у него простота и быстрота создания и настройки, а из минусов невозможность изменить управление в игре. Для примера буду использовать управление персонажем от первого лица, для третьего лица будет все аналогично.
Первая часть настроек производится в настройках проекта по пути Edit -> Project Settings -> Input

Где редактор предупреждает нас о том что этот способ deprecated. Создадим тут привязки для кнопок. Action Mapping для единоразовых нажатий, например прыжки, стрельба и Axis Mappings для продолжительных нажатий, например передвижение персонажа и взгляд. Приведу сразу готовые настройки для передвижения по кнопкам WASD, осмотру мышкой и прыжку на Space. Запомним названия действий, они нам потом понадобятся.
Начнем создание с С++ версии.
Теперь создадим С++ класс нашего персонажа. Если отсутствует раздел с С++ классами, то сделать это можно с помощью Tools -> New C++ Class..., дадим ему имя OldInputCharacter и наследуемся от Character.
После создания изменим наш GameMode на уровне, указав только что созданного персонажа в строке Default Pawn Class.
Теперь перейдем к созданию играбельного персонажа. Начнем с h файла. После всех include-ов добавим forward declaration класса камеры
| C++ | 1
| class UCameraComponent; |
|
И в тело (после конструктора в раздел Public) добавим компонент камеры
| C++ | 1
2
| UPROPERTY(VisibleAnywhere, Category = "Components")
UCameraComponent* CameraComponent; |
|
И в конец класса в public добавим объявления для callback функций для управления.
| C++ | 1
2
| void MoveForward(float Axis);
void MoveRight(float Axis); |
|
Теперь перейдем к реализации, подключим необходимые заголовки
| C++ | 1
2
| #include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h" |
|
В конструкторе зададим размеры коллизиям (стандартному компоненту класса Character c именем CapsuleComponent). Создадим компонент камеры, зададим ей иерархию (дочерняя от капсулы), координаты и включим возможность вращать персонажа камерой.
| C++ | 1
2
3
4
5
| GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->SetupAttachment(GetCapsuleComponent());
CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 64.0f));
CameraComponent->bUsePawnControlRotation = true; |
|
Теперь в методе SetupPlayerInputComponent зададим управление с помощью вызова методов BindAxis и BindAction, где первым параметром идет имя действия которое мы задавали в настройках проекта. Последним параметром идет ссылка на функцию, отвечающую за действие. Для движения параметры у функций не совпадают и поэтому используем созданные нами методы.
| C++ | 1
2
3
4
5
6
| PlayerInputComponent->BindAxis("MoveForwardOld", this, &AOldInputCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRightOld", this, &AOldInputCharacter::MoveRight);
PlayerInputComponent->BindAxis("LookHorizontalOld", this, &ACharacter::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookVerticalOld", this, &ACharacter::AddControllerPitchInput);
PlayerInputComponent->BindAction("JumpOld", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("JumpOld", IE_Released, this, &ACharacter::StopJumping); |
|
Реализуем объявленные нами методы для передвижения, у функции AddMovementInput первым параметром идет вектор направления вдоль которого будет движение.
| C++ | 1
2
3
4
5
6
7
8
9
| void AOldInputCharacter::MoveForward(float Axis)
{
AddMovementInput(GetActorForwardVector(), Axis);
}
void AOldInputCharacter::MoveRight(float Axis)
{
AddMovementInput(GetActorRightVector(), Axis);
} |
|
И теперь полностью код
OldInputCharacter.h
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| #pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "OldInputCharacter.generated.h"
class UCameraComponent;
UCLASS()
class FORALL_API AOldInputCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AOldInputCharacter();
UPROPERTY(VisibleAnywhere, Category = "Components")
UCameraComponent* CameraComponent;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void MoveForward(float Axis);
void MoveRight(float Axis);
}; |
|
OldInputCharacter.cpp
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| #include "OldInputCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
// Sets default values
AOldInputCharacter::AOldInputCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->SetupAttachment(GetCapsuleComponent());
CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 64.0f));
CameraComponent->bUsePawnControlRotation = true;
}
// Called when the game starts or when spawned
void AOldInputCharacter::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AOldInputCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void AOldInputCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForwardOld", this, &AOldInputCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRightOld", this, &AOldInputCharacter::MoveRight);
PlayerInputComponent->BindAxis("LookHorizontalOld", this, &ACharacter::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookVerticalOld", this, &ACharacter::AddControllerPitchInput);
PlayerInputComponent->BindAction("JumpOld", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("JumpOld", IE_Released, this, &ACharacter::StopJumping);
}
void AOldInputCharacter::MoveForward(float Axis)
{
AddMovementInput(GetActorForwardVector(), Axis);
}
void AOldInputCharacter::MoveRight(float Axis)
{
AddMovementInput(GetActorRightVector(), Axis);
} |
|
После запуска мы увидим следующую картинку.

Управление готово.
Теперь перейдем к BP версии.
Создадим BP класс унаслоедовав его от Character, назовем его BP_OldInputCharacter и поместим в каталог Input/Old.
Так же как и в С++ создадим ему камеру и настроим остальные параметры.
Поместим камеру в качестве дочернего элемента капсулы и высоту по Z на 64
Настроим размеры капсулы
И у камеры включим параметр Use Pawn Control Rotation
И в Event Graph зададим управление аналогично

И для проверки поменяем персонажа в GameMode нашего уровня.
Новое управление
В экспериментальном виде в виде плагина это доступно еще на версии 4.27. И только в 5 (точно не помню, но вроде 5.1 или 5.2) версии его сделали стандартным. Оно делается чуть дольше и сложнее, но зато более гибкое и поддерживает переназначение кнопок прямо в игре (в теории, не пробовал).
Управление в простейшем случае состоит из Input Action и Input Mapping Context, первые отвечают за действия, а вторые за привязку кнопок. Action и Context можно размещать не только у игрока, а у тех объектов которым нужен особый ввод. Контекстов может быть несколько и их можно размещать в контроллере игрока.
В проекте создадим папку Input/New и внутри папку Actions в которой создадим BP типа Input Action (раздел Input) с именами IA_Move, IA_Look и IA_Jump
Сменим тип у IA_Move и IA_Look на Axis2D (Vector2D)

Внутри Input/New создадим BP типа Input Mapping Context (раздел Input) с именем IMC_NewInput и откроем его редактирование
Сначала добавим все Mapping созданные ранее с помощью + около слова Mappings и выберем то что создали. Потом у каждого mapping создадим привязки клавиш, нажав + у каждого mapping. Начнем с Jump, нужна всего 1 клавиша Space, без дополнительных настроек. С Move будет посложнее, надо создать 4 клавиши и разным клавишам добавить разные модификаторы, что добавлять зависит от порядка следования, в данном случае W - Swizzle Input Axis Value, S - Swizzle Input Axis Value и Negate, A - Negate и D без модификаторов, выглядеть должно так

Выглядит сложно и непонятно, но это только на первый взгляд. Логика тут простая. Move у нас двумерный ввод, то есть по двум осям, а добавляемые кнопки добавляют ввод только по одному направлению оси, по умолчанию это ось Х. Поэтому для ввода W добавляем модификатор смены оси Swizzle Input Axis Value так как для движения вперед нужна ось Y, а для движения назад (кнопка S) нужен еще и модификатор `Negate`.
Теперь Look, добавляем всего 1 "кнопку", а точнее перемещение мышки и к ней один модификатор Negate по оси Y

Теперь перейдем к С++ варианту, настройка произведенная до этого общая, создадим класс для персонажа с именем NewInputCharacter. Как и со старым управлением добавим камеру и настройки в конструкторе
После всех include-ов добавим forward declaration для всех используемых элементов
| C++ | 1
2
3
4
5
| class UCameraComponent;
class UInputComponent;
struct FInputActionValue;
class UInputAction;
class UInputMappingContext; |
|
А так же свойства для камеры, каждого Input Action и Input Mapping Context
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| UPROPERTY(VisibleAnywhere, Category = "Components")
UCameraComponent* CameraComponent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input)
UInputAction* JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input)
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* MappingContext; |
|
И объявим пару функций для передвижения
| C++ | 1
2
3
| void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value); |
|
Теперь cpp файл. Все необходимые заголовки
| C++ | 1
2
3
4
5
| #include "EnhancedInputSubsystems.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "EnhancedInputComponent.h"
#include "Engine/LocalPlayer.h" |
|
Создание и настройка камеры и капсулы
| C++ | 1
2
3
4
5
| GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->SetupAttachment(GetCapsuleComponent());
CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 64.0f));
CameraComponent->bUsePawnControlRotation = true; |
|
В методе BeginPlay добавляем наш MappingContext с индексом 0
| C++ | 1
2
3
4
5
6
7
| if (const auto PlayerController = Cast<APlayerController>(Controller))
{
if (const auto Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(MappingContext, 0);
}
} |
|
А в SetupPlayerInputComponent назначаем для каждого Input Action свое действие-функцию
| C++ | 1
2
3
4
5
6
7
| if (const auto EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ANewInputCharacter::Move);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ANewInputCharacter::Look);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
} |
|
И реализуем функции движение и просмотра
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| void ANewInputCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
AddMovementInput(GetActorForwardVector(), MovementVector.Y);
AddMovementInput(GetActorRightVector(), MovementVector.X);
}
}
void ANewInputCharacter::Look(const FInputActionValue& Value)
{
const FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
} |
|
И код целиком
NewInputCharacter.h
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| #pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "NewInputCharacter.generated.h"
class UCameraComponent;
class UInputComponent;
struct FInputActionValue;
class UInputAction;
class UInputMappingContext;
UCLASS()
class FORALL_API ANewInputCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
ANewInputCharacter();
UPROPERTY(VisibleAnywhere, Category = "Components")
UCameraComponent* CameraComponent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input)
UInputAction* JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input)
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
UInputMappingContext* MappingContext;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
}; |
|
NewInputCharacter.cpp
| C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| #include "NewInputCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "EnhancedInputComponent.h"
#include "Engine/LocalPlayer.h"
// Sets default values
ANewInputCharacter::ANewInputCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
CameraComponent->SetupAttachment(GetCapsuleComponent());
CameraComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 64.0f));
CameraComponent->bUsePawnControlRotation = true;
}
// Called when the game starts or when spawned
void ANewInputCharacter::BeginPlay()
{
Super::BeginPlay();
if (const auto PlayerController = Cast<APlayerController>(Controller))
{
if (const auto Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(MappingContext, 0);
}
}
}
// Called every frame
void ANewInputCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ANewInputCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (const auto EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ANewInputCharacter::Move);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ANewInputCharacter::Look);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
}
}
void ANewInputCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller)
{
AddMovementInput(GetActorForwardVector(), MovementVector.Y);
AddMovementInput(GetActorRightVector(), MovementVector.X);
}
}
void ANewInputCharacter::Look(const FInputActionValue& Value)
{
const FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
} |
|
Теперь унаследуемся BP классом с именем BP_CppNewInputCharacter (сохранив в каталоге Input/New) от нашего С++ класса и настроим наши Input компоненты
И как всегда поменяем игрока на новый класс в GameMode
И сделаем то же самое в BP, для простоты и чтобы не повторяться, в качестве основы скопируем переместим и переименуем BP от старого ввода, поместим его по адресу Input/New с именем BP_NewInputCharacter. У нас уже будет камера и остальные настройки, только удалим все ноды из Event Graph. Теперь настроим управление

И в GameMode поставим нашего нового игрока
Запустим игру и убедимся что все работает

И упакуем исходники проекта, только то что нужно, это в движке делается автоматически с помощью File -> Zip Project. Приложу то, что запаковалось в архив ForAll.zip
|