Showing

[Unreal] Enhanced Input System 캐릭터 이동 설정 본문

Unreal

[Unreal] Enhanced Input System 캐릭터 이동 설정

RabbitCode 2023. 12. 28. 16:55

Mapping Context 

Build.cs 추가

아래와 같이

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json", "JsonUtilities", "EnhancedInput" });

"EnhancedInput" 을 추가해준다.

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class TracesoftheFox : ModuleRules
{
	public TracesoftheFox(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json", "JsonUtilities", "EnhancedInput" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

이동 설정

ws키가 x축을 기준으로 상하를 움직이는 것이라면 아래와 같이 설정해준다.(원래 디폴트값이 xyz이지만 명시 상태)

ws키가 x축을 기준으로 상하를 움직이는 것이라면 아래와 같이 설정해준다.

UEnhancedInputLocalPlayerSubsystem를 부르는 코드는 아래와 같이 항시 고정적으로 쓰면 된다.

void ASViewCharacter::BeginPlay()
{
	Super::BeginPlay();
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		//향상된 인풋 시스템들을 관리하는 단 하나의 싱글톤 매니저 같은 UEnhancedInputLocalPlayerSubsystem
		UEnhancedInputLocalPlayerSubsystem* Subsystem
			= ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
		if (true == IsValid(Subsystem)) {
			Subsystem->AddMappingContext(PlayerCharacterInputMappingContext, 0);
		}
	}
}

 

void AMyPlayer::BeginPlay()
{
	Super::BeginPlay();
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem
			= ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) {
			Subsystem->AddMappingContext(PlayerMappingContext, 0);
		}
	}
	jumpNiagara = GetComponentByClass<UNiagaraComponent>();
	jumpNiagara->SetVisibility(false);
}

물론 헤더에서 매핑 컨텍스트를 정의해주고, 블루프린트클래스에서 매핑컨텍스트를 넣어주어야 한다.

public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASViewCharacter")
	TObjectPtr<class USInputConfigData> PlayerCharacterInputConfigData;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASViewCharacter")
	TObjectPtr<class UInputMappingContext> PlayerCharacterInputMappingContext;
	
	EViewMode CurrentViewMode = EViewMode::None;

그리고 아래의 코드와 달리 위의 코드에서는 Input 데이터 묶음을 가져와서 훨씬 코드 가독성이 좋아졌는데,

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Camera")
	class USpringArmComponent* springArmComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
	class UCameraComponent* cameraComp;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputMappingContext* PlayerMappingContext;

	UPROPERTY(VisibleAnywhere, Category = "Weapon")
	class UStaticMeshComponent* staffMesh;

	UPROPERTY(EditAnywhere, Category = "FX")
	class UNiagaraComponent* jumpNiagara;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* MoveIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* LookUpIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* TurnIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* JumpIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* FireIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* InteractionIA;
	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* LineTraceIA;

이 설정은 아래 캡처와 같이 해주면 된다.

 

EnhancedInput을 이용한 Bind

void ASViewCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
	if (true == ::IsValid(EnhancedInputComponent)) {
		EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->MoveAction, ETriggerEvent::Triggered, this, &ASViewCharacter::Move);
		EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->LookAction, ETriggerEvent::Triggered, this, &ASViewCharacter::Look);
	}
}
void AMyPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
		EnhancedInputComponent->BindAction(MoveIA, ETriggerEvent::Triggered, this, &AMyPlayer::Move);
		EnhancedInputComponent->BindAction(LookUpIA, ETriggerEvent::Triggered, this, &AMyPlayer::LookUp);
		EnhancedInputComponent->BindAction(TurnIA, ETriggerEvent::Triggered, this, &AMyPlayer::Turn);
		EnhancedInputComponent->BindAction(JumpIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputJump);
		EnhancedInputComponent->BindAction(FireIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputFire);
		EnhancedInputComponent->BindAction(InteractionIA, ETriggerEvent::Started, this, &AMyPlayer::InteractionPositive);
	}
}

 

또한 입력 상태에 따라 원하는 구현 결과가 다를텐데 아래 정의된 바에 따라서 이벤트를 지정해주면 된다.

/**
* Trigger events are the Action's interpretation of all Trigger State transitions that occurred for the action in the last tick
*/
UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class ETriggerEvent : uint8
{
	// No significant trigger state changes occurred and there are no active device inputs
	None		= (0x0)		UMETA(Hidden),

	// Triggering occurred after one or more processing ticks
	Triggered	= (1 << 0),	// ETriggerState (None -> Triggered, Ongoing -> Triggered, Triggered -> Triggered)
	
	// An event has occurred that has begun Trigger evaluation. Note: Triggered may also occur this frame.
	Started		= (1 << 1),	// ETriggerState (None -> Ongoing, None -> Triggered)

	// Triggering is still being processed
	Ongoing		= (1 << 2),	// ETriggerState (Ongoing -> Ongoing)

	// Triggering has been canceled
	Canceled	= (1 << 3),	// ETriggerState (Ongoing -> None)

	// The trigger state has transitioned from Triggered to None this frame, i.e. Triggering has finished.
	// NOTE: Using this event restricts you to one set of triggers for Started/Completed events. You may prefer two actions, each with its own trigger rules.
	// TODO: Completed will not fire if any trigger reports Ongoing on the same frame, but both should fire. e.g. Tick 2 of Hold (= Ongoing) + Pressed (= None) combo will raise Ongoing event only.
	Completed	= (1 << 4),	// ETriggerState (Triggered -> None)
};
ENUM_CLASS_FLAGS(ETriggerEvent)

 

회전 설정

마우스를 따라 위 아래가 맞으려면 y축 negate 해주어야 한다.

 

회전 & 이동 코드

아래 Move에서 [pitch] [yaw] [roll] 을 매트릭스화 해주어야 GetUnitAxis를 쓸 수 있다.

[p] [ ] [ ]

[ ] [y] [ ]

[ ] [ ] [r]

각 축의 Unit Axis 를 뽑아오려면 매트릭스화 필수(x축은 red, y축은 green)

회전된 상태의 매트릭스에서 x 방향과 y방향 축을 뽑아서 각각을 앞벡터, 우벡터로 지정해주고

AddMovement input 함수를 이용해서 키보드 입력이 들어온 만큼 이동을 진행시키는 로직이다.

void ASViewCharacter::Move(const FInputActionValue& InValue)
{
	// 이동시 캐릭터는 위 아래로 쳐다보지 않을 것이므로
	// GetController의 rot에서 pitch가 아닌 Yaw값만 가지고 온다.
	const FRotator ControlRotation = GetController()->GetControlRotation();
	const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);
	// 플레이어의 회전 의지 중 Yaw 성분으로만 전진 방향을 결정하고자 함.
	// GetController의 의도에서 Yaw값만을 추출한 벡터 자체의 회전행렬의 결과 매트릭스에서,
	// 유닛 벡터 중 x축을 ForwardVector로 지정하겠다. 
	const FVector ForwardVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	const FVector RightVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

	FVector2D MovementVector = InValue.Get<FVector2D>();
	AddMovementInput(ForwardVector, MovementVector.X); // 전, 후
	AddMovementInput(RightVector, MovementVector.Y); // 좌, 우
}

void ASViewCharacter::Look(const FInputActionValue& InValue)
{
	if (nullptr != GetController()) {
		FVector2D LookVector = InValue.Get<FVector2D>();
		AddControllerYawInput(LookVector.X * 0.5f);
		AddControllerPitchInput(LookVector.Y);
	}
	if (nullptr != GetController())
	{
		FVector2D LookVector = InValue.Get<FVector2D>();
		AddControllerYawInput(LookVector.X);
		// 아까 IMC에서 X에다가 마우스 좌우 값을 넣었음.
		AddControllerPitchInput(LookVector.Y);
	}
}

picth가 통상 위아래를 쳐다보는 것과 이어지고,

yaw가 고개를 좌우로 돌리는 것과 이어진다.

혹은

아래와 같이 코드를 작성할 수도 있다. 헤더에 공유 벡터를 하나 정의해주고 tick에서 돌리는 스타일이다.

private:
	FVector moveDirection;
// Called every frame
void AMyPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (fireReady == false) {
		FireCoolTimer(fireCoolTime, DeltaTime);
	}

	const TArray<USceneComponent*>& AttachChildren = GetRootComponent()->GetAttachChildren();
	const int32 Count = AttachChildren.Num();


	// 이동방향을 컨트롤 방향 기준으로 변환
	moveDirection = FTransform(GetControlRotation()).TransformVector(moveDirection);

	/*
	// 플레이어 이동 - 등속운동
	FVector P0 = GetActorLocation();//현재 위치
	FVector vt = moveDirection * moveSpeed * DeltaTime; //이동거리
	FVector P = P0 + vt;
	SetActorLocation(P);
	*/

	AddMovementInput(moveDirection);

	// 방향 초기화
	// 방향이 누적되지 않게
	moveDirection = FVector::ZeroVector;
}

// Called to bind functionality to input
void AMyPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
		EnhancedInputComponent->BindAction(MoveIA, ETriggerEvent::Triggered, this, &AMyPlayer::Move);
		EnhancedInputComponent->BindAction(LookUpIA, ETriggerEvent::Triggered, this, &AMyPlayer::LookUp);
		EnhancedInputComponent->BindAction(TurnIA, ETriggerEvent::Triggered, this, &AMyPlayer::Turn);
		EnhancedInputComponent->BindAction(JumpIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputJump);
		EnhancedInputComponent->BindAction(FireIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputFire);
		EnhancedInputComponent->BindAction(InteractionIA, ETriggerEvent::Started, this, &AMyPlayer::InteractionPositive);
	}
}

void AMyPlayer::Move(const FInputActionValue& Value)
{
	UE_LOG(LogTemp, Log, TEXT("Log Message %f"), moveSpeed);
	const FVector _currentValue = Value.Get<FVector>();
	if (Controller) {
		moveDirection.Y = _currentValue.X;
		moveDirection.X = _currentValue.Y;
	}
	//FVector newLocation = GetActorLocation() + _currentValue;
	//SetActorLocation(newLocation);
}

void AMyPlayer::LookUp(const FInputActionValue& Value)
{
	// mouse y - 한 축의 값(float)
	const float _currentValue = Value.Get<float>();
	APawn::AddControllerPitchInput(_currentValue);
}

void AMyPlayer::Turn(const FInputActionValue& Value)
{
	// mouse x - 한 축의 값(float)
	const float _currentValue = Value.Get<float>();
	APawn::AddControllerYawInput(_currentValue);
}

컨트롤러와 pawn, camera, spring arm의 관계

Character 클래스는 기본적으로 bUseControllerRotationYaw 속성이 true

컨트롤 로테이션의 Yaw 값이 캐릭터의 Rotation Yaw 값에 즉시 동기화.

 

 

Pawn - SpringArmComponent

bUsePawnControlRotation 속성에 의해서 폰의 회전 값이 연동될지 안될지 결정

bUsePawnControlRotation이 false라면 폰이 회전해도 스프링암은 회전하지 않음. 컷씬을 촬영할 때 활용 가능.

 

bDoCollisionTest 속성은 폰과 카메라 사이에 벽이 있을 경우 카메라를 폰쪽으로 당김.

 

<bp>use controller- 검색

<카메라> usepawn- 검색

<스피링암> bDoCollisionTest : 벽에 부딪힐 때 카메라가 벽을 넘어가게 할 지 안 넘어가게 할

bInherit- 속성에 의해서 Root Component의 회전 값이 연동될지 안될지 결정

가령 Root가 Pawn이면, Controller회전값을 가지고 있을 것이고 스프링암이 그것을 가져올지 말지를 결정할 수 있음

ex ) backView

SpringArmComponent->TargetArmLength = 400.f;
SpringArmComponent->SetRelativeRotation(FRotator::ZeroRotator);
// ControlRotation이 Pawn의 회전과 동기화 -> Pawn의 회전이 SprintArm의 회전 동기화. 이로 인해 SetRotation()이 무의미.

bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;

SpringArmComponent->bUsePawnControlRotation = true;
SpringArmComponent->bDoCollisionTest = true;
SpringArmComponent->bInheritPitch = true;
SpringArmComponent->bInheritYaw = true;
SpringArmComponent->bInheritRoll = false;