이 프로젝트는 GameplayAbilitySystem, Enhanced Input, Common UI에 대해
깊이 들어가기 위한 토이프로젝트입니다.
한국어 자료가 없어서 베이스부터 활용까지 정리해보았습니다.
1. Test project 생성
- UE 5.1.1
- ThirdPerson
- C++
- Starter Content 포함
- 레이트레이싱 포함
- 프로젝트 이름은 GAS
- Editor Preference > live coding 체크 해제.
- GAS.Build.cs 파일 수정
<hide/>
// GAS.Build.cs
using UnrealBuildTool;
public class GAS : ModuleRules
{
public GAS(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.Add("GAS/Public");
PrivateIncludePaths.Add("GAS/Private");
PublicDependencyModuleNames.AddRange(new string[]
{
// Initial Modules
"Core", "CoreUObject", "Engine", "InputCore",
});
}
}
2. 클래스 생성
2.1 GASGlobal 클래스
- 새로운 C++ 클래스 > 부모 클래스로 None 지정 > "GASGlobal" 클래스
- GASGlobal 클래스에서 전역 enum 값들을 관리.
<hide/>
// GASGlobal.h
#pragma once
#include "CoreMinimal.h"
#define CHARACTERWALKSPEED (500.f)
#define CHARACTERRUNSPEED (1000.f)
#define CHARACTERSPRINTSPEED (1500.f)
2.2 GASCharacter 클래스
- 프로젝트에 있는 GASCharacter 클래스 재활용
- Content > 새 폴더 "GAS" 생성
GAS > 새 폴더 "Blueprints" 생성
- Content > GAS > Blueprints
새로운 블루프린트 클래스 > GASCharacter 부모 클래스 > "BP_GASCharacter"
Details > SkeletalMesh에 아무거나 지정.
Details > Transform > Location Z값을 -100, Rotation Z값을 -90 [필요할 때만.]
<hide/>
// GASCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "GASCharacter.generated.h"
UCLASS()
class AGASCharacter : public ACharacter
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class UCameraComponent* FollowCamera;
public:
AGASCharacter();
public:
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};
<hide/>
// GASCharacter.cpp
#include "GASCharacter.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GASGlobal.h"
AGASCharacter::AGASCharacter()
{
PrimaryActorTick.bCanEverTick = false;
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
#pragma region InitializeCharacterMovement
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = CHARACTERWALKSPEED;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f; // Relates to game pad's laterncy moving.
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
#pragma endregion
#pragma region InitializeSpringArm
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
#pragma endregion
#pragma region InitializeCamera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
#pragma endregion
}
2.3 GASPlayerController 클래스
- 새로운 C++ 클래스 > Player Controller 부모 클래스 > "GASPlayerController" 클래스
- Content > GASGAS > Blueprints > Character
새로운 블루프린트 클래스 > GASPlayerController 부모 클래스 > "BP_GASPlayerController"
<hide/>
// GASPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GASPlayerController.generated.h"
UCLASS()
class GAS_API AGASPlayerController : public APlayerController
{
GENERATED_BODY()
};
2.4 GASGameMode 클래스
- 기존에 생성되어 있던 GASGameMode 재활용.
- Content > GAS > Blueprints > Character
새로운 블루프린트 클래스 > GASGameMode 부모 클래스 > "BP_GASGameMode" 클래스
- World Settings > GameMode Override에 BP_GASGameMode 지정.
Default Pawn Class에 BP_GASCharacter 지정.
Player Controller Class에 BP_GASPlayerController 지정.
<hide/>
// GASGameMode.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "GASGameMode.generated.h"
UCLASS(minimalapi)
class AGASGameMode : public AGameModeBase
{
GENERATED_BODY()
};
<hide/>
// GASGameMode.cpp
#include "GASGameMode.h"
3. Enhanced Input
3.1 Enhanced Input을 위한 프로젝트 설정
- Edit > Plugins > Enhanced Input 검색 후 체크해서 활성화
- .uproject 파일 수정
- .Build.cs 파일 수정
- Project Settings > Input > Default Player Input Class를 EnhancedPlayerInput
Default Input Component Class를 EnhancedInputComponent로 지정해줘야함.
<hide/>
// GAS.uproject
{
"FileVersion": 3,
"EngineAssociation": "5.1",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "GAS",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
]
}
],
"Plugins": [
{
"Name": "ModelingToolsEditorMode",
"Enabled": true,
"TargetAllowList": [
"Editor"
]
},
{
"Name": "EnhancedInput",
"Enabled": true
}
]
}
<hide/>
// GAS.Build.cs
using UnrealBuildTool;
public class GAS : ModuleRules
{
public GAS(ReadOnlyTargetRules Target) : base(Target)
{
...
PublicDependencyModuleNames.AddRange(new string[]
{
...
// Input Modules
"EnhancedInput",
// GameplayAbilitySystem
"GameplayTags",
});
}
}
3.2 Input Action 만들기
- Content > GAS > 새폴더 "EnhancedInputs"
새 InputAction 생성
"IA_GASMove" / "IA_GASJump" / "IA_GASLook" 생성
- IA_GASJump는 Action mapping임. 따라서 bool로 설정.
IA_GASMove와 IA_GASLook은 Axis mapping임. 따라서 Vector2D.
3.3 InputConfigData 클래스
- 새로운 C++ 클래스 > DataAsset 부모 클래스 > "GASInputConfigData" 클래스
입력키들을 선언 및 정의해둔 클래스.
쉬움 코드와 어려움 코드를 나눠두었음. 필자는 어려움 코드를 기반으로 정리.
FGameplayTag를 앞으로 자주 활용할 예정이기 때문.
- Content > GAS > EnhancedInputs
새로운 DataAsset 클래스 > GASInputConfigData 부모 클래스 > "DA_GASInputConfigData"
- DA_GASInputConfigData > Details에서 적절하게 Input Action 지정.
<hide/>
// GASInputConfigData.h [쉬움 코드. 결국 핵심은 이 코드. 필자는 이 코드 안씀.]
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GASInputConfigData.generated.h"
UCLASS()
class GAS_API UGASInputConfigData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
class UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
class UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
class UInputAction* JumpAction;
};
<hide/>
// GASInputConfigData.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "InputAction.h"
#include "Engine/DataAsset.h"
#include "GASInputConfigData.generated.h"
USTRUCT(BlueprintType)
struct FTagBindingInputAction
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly)
const UInputAction* InputAction = nullptr;
UPROPERTY(EditDefaultsOnly, meta = (Categories = "InputTag"))
FGameplayTag InputTag;
};
UCLASS()
class GAS_API UGASInputConfigData : public UDataAsset
{
GENERATED_BODY()
public:
const UInputAction* FindNativeInputActionByTag(const FGameplayTag& NewInputTag, bool bLogNotFound = true) const;
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputActions"))
TArray<FTagBindingInputAction> NativeInputActions;
};
<hide/>
// GASInputConfigData.cpp
#include "GASInputConfigData.h"
const UInputAction* UGASInputConfigData::FindNativeInputActionByTag(const FGameplayTag& NewInputTag,
bool bLogNotFound) const
{
for (const FTagBindingInputAction& Action : NativeInputActions)
{
if (Action.InputAction && Action.InputTag.MatchesTagExact(NewInputTag))
{
return Action.InputAction;
}
}
if (bLogNotFound)
{
UE_LOG(LogTemp, Error, TEXT("Can't find NativeInputAction for InputTag [%s] on InputConfig [%s]."), *NewInputTag.ToString(), *GetNameSafe(this));
}
return nullptr;
}
3.4 Input Mapping Context
- Content > GAS > EnhancedInputs
새 Input 클래스 > Input Mapping Context > "IMC_GASDefault"
3.5 Character 클래스와 Input Mapping Context
- IMC를 Player Controller와 연결할 수도 있겠지만,
TPS 게임 특성 상 비행기를 조종 하게 될 수도 있기 때문에 플레이어에 연결함.
- 아래 코드를 컴파일 후, BP_GASCharacter > Details에서
Input Actions > DA_GASInputConfigData
Default Input Mapping Context > IMC_GASDefault 지정.
<hide/>
// GASCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "GASGlobal.h"
#include "InputActionValue.h"
#include "GASCharacter.generated.h"
UCLASS()
class AGASCharacter : public ACharacter
{
...
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UGASInputConfigData* InputActions;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
class UInputMappingContext* DefaultInputMappingContext;
public:
AGASCharacter();
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
virtual void BeginPlay() override;
public:
...
private:
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
};
<hide/>
// GASCharacter.cpp
...
#include "Components/InputComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GASInputConfigData.h"
AGASCharacter::AGASCharacter()
{
PrimaryActorTick.bCanEverTick = false;
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
InputActions = CreateDefaultSubobject<UGASInputConfigData>(TEXT("InputActions"));
...
}
void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
{
/* 쉬움 코드 기준 바인딩 방법. 필자는 이거 안씀.
EnhancedInputComponent->BindAction(InputActions->JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(InputActions->JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EnhancedInputComponent->BindAction(InputActions->MoveAction, ETriggerEvent::Triggered, this, &AGASCharacter::Move);
EnhancedInputComponent->BindAction(InputActions->LookAction, ETriggerEvent::Triggered, this, &AGASCharacter::Look);
*/
#pragma region BindNativeInputActions
/*
* 익명함수를 정의해서 깔끔하게 바인딩 하는 방법.
* 문제점은 바인딩될 메서드가 인자를 받을 때. 이걸 처리해주기 귀찮음. 결국 너무 어렵게 길을 가게 될 수도.
*/
/*
auto BindNativeInputAction = [EnhancedInputComponent](UGASInputConfigData* NewInputConfigData, FGameplayTag NewInputTag, ETriggerEvent NewTriggerEvent, ThisClass* NewThisClass, FEnhancedInputActionHandlerSignature::TMethodPtr<ThisClass> NewMappingMethod)
{
if (const UInputAction* NativeInputAction = NewInputConfigData->FindNativeInputActionByTag(NewInputTag))
{
EnhancedInputComponent->BindAction(NativeInputAction, NewTriggerEvent, NewThisClass, NewMappingMethod);
}
};
BindNativeInputAction(InputActions, FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump")), ETriggerEvent::Triggered, this, &ACharacter::Jump);
BindNativeInputAction(InputActions, FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump")), ETriggerEvent::Completed, this, &ACharacter::StopJumping);
BindNativeInputAction(InputActions, FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Move")), ETriggerEvent::Triggered, this, &ThisClass::Move); 이 경우가 문제됨.
*/
/*
* 반복문으로 순회하는 방법.
* 바인딩될 메서드를 인자로 만들기에 어려움이 있음. 다음에 방법을 찾아보자.
*/
/*
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
ETriggerEvent::Completed, this, &ACharacter::StopJumping);
for (FTagBindingInputAction NativeTagBindingInputAction : InputActions->NativeInputActions)
{
if (NativeTagBindingInputAction.InputTag == FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump")))
{
continue;
}
EnhancedInputComponent->BindAction(NativeTagBindingInputAction.InputAction, ETriggerEvent::Triggered, this, &ThisClass::몰?루);
}
*/
// 필자는 그냥 지정함. 하드 코딩이지만, 움직임이 별로 없을때는 가독성 측면에서 이게 더 좋은듯.
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
ETriggerEvent::Completed, this, &ACharacter::StopJumping);
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Move"))),
ETriggerEvent::Triggered, this, &ThisClass::Move);
EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Look"))),
ETriggerEvent::Triggered, this, &ThisClass::Look);
#pragma endregion
}
}
void AGASCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultInputMappingContext, 0);
}
}
}
void AGASCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void AGASCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
3.6 GASCharacterAnimInstance 클래스
- 새 C++ 클래스 > AnimInstance 부모 클래스 > "GASCharacterAnimInstance" 클래스
Content > GAS > Blueprint > Character
새 애니메이션 블루프린트 클래스 > 적절한 스켈레톤 선택
"ABP_GASCharacter" 클래스 생성
- ABP_GASCharacter > Class Settings > Parent Class를 GASCharacterAnimInstance로 지정.
- BP_GASCharacter > Anim Class를 ABP_GASCharacter_C로 지정.
사실 GASCharacter C++ 클래스에 하드코딩으로 에셋을 지정할 수도 있으나,
이는 경로정보가 절대 안바뀔것이라는 가정하에 작성하는 것.
초기에 작성하고 아주 나중에 누군가가 폴더 정리를 한다면, 모두 다시 작성해야함.
따라서 블루프린트에서 에셋을 지정해주거나, ini 파일로 설정해주는 것이 올바른 방법.
<hide/>
// GASCharacterAnimInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "GASCharacterAnimInstance.generated.h"
UCLASS()
class GAS_API UGASCharacterAnimInstance : public UAnimInstance
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = CharacterCurrentState, meta = (AllowPrivateAccess = "true"))
FVector CurrentVelocity;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = CharacterCurrentState, meta = (AllowPrivateAccess = "true"))
float CurrentSpeed;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Movement, Meta=(AllowPrivateAccess=true))
bool IsInAir;
public:
virtual void NativeBeginPlay() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
};
<hide/>
// GASCharacterAnimInstance.cpp
#include "GASCharacterAnimInstance.h"
#include "GASCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
void UGASCharacterAnimInstance::NativeBeginPlay()
{
Super::NativeBeginPlay();
}
void UGASCharacterAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
APawn* OwnerPawn = TryGetPawnOwner();
if (true == ::IsValid(OwnerPawn))
{
CurrentSpeed = OwnerPawn->GetVelocity().Size();
ACharacter* OwnerCharacter = Cast<ACharacter>(OwnerPawn);
check(nullptr != OwnerCharacter);
IsInAir = OwnerCharacter->GetCharacterMovement()->IsFalling();
}
}
3.7 애니메이션 블루프린트
- State Machine을 활용하여 Idle / Walk / Jump 구현
'Unreal > GAS 토이프로젝트' 카테고리의 다른 글
[GameplayAbilitySystem 찍먹 프로젝트 - 2] Gameplay Ability System (2) | 2023.03.26 |
---|
댓글