1일1알

Input 본문

언리얼/Lyra 프로젝트 분석

Input

영춘권의달인 2024. 7. 6. 16:49

언리얼 5부터는 EnhancedInputComponent를 사용하여 입력을 처리한다.

간단하게 설명하면 Input Action에서 행동에 대한 정의를 하고

Input Action들을 모아서 Input Action들에 대해 어떤 입력에 대응할지를 정의하는 Input Mapping Context를 만든다.

그리고 EnhancedInputComponent에 Input Action에 대응하는 함수를 만들어서 입력에 대한 처리를 한다.

 

이 EnhancedInputComponent를 Project Settings의 Input탭에서 설정할 수 있는데, Lyra 프로젝트에서는 EnhancedInputComponent를 상속받은 LyraInputComponent를 만들어서 사용하고 있다.

 

template<class UserClass, typename FuncType>
void ULyraInputComponent::BindNativeAction(const ULyraInputConfig* InputConfig, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound)
{
	check(InputConfig);
	if (const UInputAction* IA = InputConfig->FindNativeInputActionForTag(InputTag, bLogIfNotFound))
	{
		BindAction(IA, TriggerEvent, Object, Func);
	}
}

ULyraInputComponent의 BindNativeAction함수에서 입력과 함수를 매핑해주는 BindAction을 호출해주기 때문에 이부분을 살펴보도록 하겠다.

인자로 ULyraInputConfig라는 타입을 받아주고 있다. 

 

USTRUCT(BlueprintType)
struct FLyraInputAction
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UInputAction> InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

/**
 * ULyraInputConfig
 *
 *	Non-mutable data asset that contains input configuration properties.
 */
UCLASS(BlueprintType, Const)
class ULyraInputConfig : public UDataAsset
{
	GENERATED_BODY()

public:

	ULyraInputConfig(const FObjectInitializer& ObjectInitializer);

	UFUNCTION(BlueprintCallable, Category = "Lyra|Pawn")
	const UInputAction* FindNativeInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) const;

	UFUNCTION(BlueprintCallable, Category = "Lyra|Pawn")
	const UInputAction* FindAbilityInputActionForTag(const FGameplayTag& InputTag, bool bLogNotFound = true) const;

public:
	// List of input actions used by the owner.  These input actions are mapped to a gameplay tag and must be manually bound.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<FLyraInputAction> NativeInputActions;

	// List of input actions used by the owner.  These input actions are mapped to a gameplay tag and are automatically bound to abilities with matching input tags.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (TitleProperty = "InputAction"))
	TArray<FLyraInputAction> AbilityInputActions;
};

NativeInputActions라는 FLyraInputAction들을 가지고 있는 TArray에서 입력에 관한 정보들을 가지고 있는 것 같다. 

FLyraInputAction를 살펴보면 구조체로 되어있고, InputAction과 GameplayTag를 가지고 있다.

InputAction을 GameplayTag를 통해 사용할 수 있도록 구조체로 묶어놓은 것 같다.

Find~함수들은 GameplayTag를 이용해 TArray에 있는 FLyraInputAction를 찾는 함수이다.

 

template<class UserClass, typename FuncType>
void ULyraInputComponent::BindNativeAction(const ULyraInputConfig* InputConfig, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound)
{
	check(InputConfig);
	if (const UInputAction* IA = InputConfig->FindNativeInputActionForTag(InputTag, bLogIfNotFound))
	{
		BindAction(IA, TriggerEvent, Object, Func);
	}
}

다시 ULyraInputComponent의 BindNativeAction함수로 돌아오면 이 함수는 GameplayTag를 이용해 ULyraInputConfig에 GameplayTag와 매칭되는 InputAction이 있다면 해당 InputAction과 인자로 받은 함수를 연결해주는 역할을 한다.

대충 무슨 역할을 하는지는 알겠는데 그럼 이 함수는 어디서 호출되는지가 궁금해진다.

 

Break Point를 잡고 에디터를 켠 뒤 플레이 버튼을 누르는 시점에 잡혔다.

호출 스택을 보니 굉장히 복잡한데... 우선 바로 아래에 있는 함수를 살펴보면 LyraHeroComponent의 InitializePlayerInput라는 함수에서 호출이 된다.

 

void ULyraHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
	check(PlayerInputComponent);

	const APawn* Pawn = GetPawn<APawn>();
	if (!Pawn)
	{
		return;
	}

	const APlayerController* PC = GetController<APlayerController>();
	check(PC);

	const ULyraLocalPlayer* LP = Cast<ULyraLocalPlayer>(PC->GetLocalPlayer());
	check(LP);

	UEnhancedInputLocalPlayerSubsystem* Subsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
	check(Subsystem);

	Subsystem->ClearAllMappings();

	if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
	{
		if (const ULyraPawnData* PawnData = PawnExtComp->GetPawnData<ULyraPawnData>())
		{
			if (const ULyraInputConfig* InputConfig = PawnData->InputConfig)
			{
				for (const FInputMappingContextAndPriority& Mapping : DefaultInputMappings)
				{
					if (UInputMappingContext* IMC = Mapping.InputMapping.Get())
					{
						if (Mapping.bRegisterWithSettings)
						{
							if (UEnhancedInputUserSettings* Settings = Subsystem->GetUserSettings())
							{
								Settings->RegisterInputMappingContext(IMC);
							}
							
							FModifyContextOptions Options = {};
							Options.bIgnoreAllPressedKeysUntilRelease = false;
							// Actually add the config to the local player							
							Subsystem->AddMappingContext(IMC, Mapping.Priority, Options);
						}
					}
				}

				// The Lyra Input Component has some additional functions to map Gameplay Tags to an Input Action.
				// If you want this functionality but still want to change your input component class, make it a subclass
				// of the ULyraInputComponent or modify this component accordingly.
				ULyraInputComponent* LyraIC = Cast<ULyraInputComponent>(PlayerInputComponent);
				if (ensureMsgf(LyraIC, TEXT("Unexpected Input Component class! The Gameplay Abilities will not be bound to their inputs. Change the input component to ULyraInputComponent or a subclass of it.")))
				{
					// Add the key mappings that may have been set by the player
					LyraIC->AddInputMappings(InputConfig, Subsystem);

					// This is where we actually bind and input action to a gameplay tag, which means that Gameplay Ability Blueprints will
					// be triggered directly by these input actions Triggered events. 
					TArray<uint32> BindHandles;
					LyraIC->BindAbilityActions(InputConfig, this, &ThisClass::Input_AbilityInputTagPressed, &ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);

					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Move, ETriggerEvent::Triggered, this, &ThisClass::Input_Move, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Mouse, ETriggerEvent::Triggered, this, &ThisClass::Input_LookMouse, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Stick, ETriggerEvent::Triggered, this, &ThisClass::Input_LookStick, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Crouch, ETriggerEvent::Triggered, this, &ThisClass::Input_Crouch, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_AutoRun, ETriggerEvent::Triggered, this, &ThisClass::Input_AutoRun, /*bLogIfNotFound=*/ false);
				}
			}
		}
	}

	if (ensure(!bReadyToBindInputs))
	{
		bReadyToBindInputs = true;
	}
 
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APlayerController*>(PC), NAME_BindInputsNow);
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APawn*>(Pawn), NAME_BindInputsNow);
}

일단 클래스 이름에서 내가 조종하는 캐릭터의 컴포넌트로 부착된 클래스라는 것을 추측해볼 수 있다.

 

실제로 처음에 스폰되는 캐릭터에서 LyraHeroComponent를 찾을 수 있다.

 

이제 InitializePlayerInput함수가 어떤식으로 동작하는지 살펴보면

1. ULyraPawnExtensionComponent라는 클래스를 통해 ULyraPawnData를 가져온다.

2. ULyraPawnData에 저장되어있는 ULyraInputConfig를 가져온다.

3. 블루프린트에서 넣어준 IMC들을 EnhancedInput 관련 Subsystem에 등록해준다.

4. 인자로 받은 PlayerInputComponent를 ULyraInputComponent로 캐스팅하여 BindNativeAction함수를 호출해 입력에 대한 동작을 연결해준다.

 

눈여겨볼만한 점은 UEnhancedInputComponent를 상속받아서 추가적인 기능을 제작할 수 있도록 한 것과 InputAction과 GameplayTag를 묶어서 관리하여 GameplayTag를 통해 제어할 수 있도록  것인 듯하다.

 

일단 기본적인 입력에 대한 부분만 알아봤고 나머지는 다른 부분들을 분석하고 추가해야겠다.

'언리얼 > Lyra 프로젝트 분석' 카테고리의 다른 글

Lyra 분석 - IGameFrameworkInitStateInterface  (0) 2024.07.10
Lyra분석 - LyraGameMode, experience  (0) 2024.07.08
LyraAssetManager  (0) 2024.07.05