[UE5] 코드 리팩토링 및 버그 수정

연하·2024년 6월 3일
0

Trapper

목록 보기
7/32
post-thumbnail

1차 발표가 끝나고 방학 푸욱 쉬고 돌아왔다. 방학동안은 언리얼 네트워크 공부를 조금 해두고 왔다. 공부하는 내내 너무 어지러웠다.. 아무튼 6월의 2차 발표 목표는 2인 멀티플레이가 가능하도록 구현하는 것! 다시 힘내서 달려봐야지 :)

네트워크 로직을 생각하기 이전에, 기존에 짜뒀던 캐릭터 관련 코드가 너무 마음에 들지 않아 오늘은 코드를 살짝 수정하기로 결정했다. 앞으로 조금씩 계속 고쳐나가다보면 마음에 드는 코드가 되지 않을까.

Item 부모 클래스 제작

void ATrapperPlayer::FindMagneticItem()
{
	FVector PlayerLocation = GetActorLocation();
	TArray<FOverlapResult> OverlapResults;
	FCollisionShape Sphere = FCollisionShape::MakeSphere(MagneticPullRange);

	bool bHasOverlap = GetWorld()->OverlapMultiByChannel(
		OverlapResults,
		PlayerLocation,
		FQuat::Identity,
		ECC_GameTraceChannel3,
		Sphere
	);

	if (bHasOverlap)
	{
		for (auto& Result : OverlapResults)
		{
			AActor* OverlappedActor = Result.GetActor();

			if (OverlappedActor)
			{
				MagneticPull(OverlappedActor);
			}
		}
	}
}

void ATrapperPlayer::MagneticPull(AActor* Item)
{
	if (!Item) return;

	FVector ActorTolerance(0.f, 0.f, 20.f);

	FVector PlayerLocation = GetActorLocation() + ActorTolerance;
	FVector ItemLocation = Item->GetActorLocation();
	FVector NewPosition = FMath::VInterpTo(ItemLocation, PlayerLocation, GetWorld()->GetDeltaSeconds(), 5.f);
	
	const float Tolerance = 20.f;
	if (ItemLocation.Equals(PlayerLocation, Tolerance))
	{
		Item->Destroy();
	}

	Item->SetActorLocation(NewPosition);
}

기존에는 아이템을 탐색하는 것도, 아이템을 끌어오는 것도 플레이어에서 담당하고 있었다. 플레이어는 아이템 탐색까지만 하고, 탐색이 완료된 아이템은 아이템 코드에서 직접 플레이어쪽으로 이동하고 Destory되도록 변경해줄 것이다. 플레이어의 MagneticPull() 함수는 전부 지워주었다.

// AItem.h

UCLASS()
class TRAPPERPROJECT_API AItem : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AItem();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

public:
	void BeDrawnToPlayer();
	void SetCanBePulled(AActor* Player);
};

먼저 아이템 클래스를 하나 생성해주었다.

// AItem.cpp

AItem::AItem()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AItem::BeginPlay()
{
	Super::BeginPlay();
	SetActorTickEnabled(false);
}

// Called every frame
void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	BeDrawnToPlayer();
}

void AItem::BeDrawnToPlayer()
{
	FVector ActorTolerance(0.f, 0.f, 20.f);

	FVector PlayerLocation = Owner->GetActorLocation() + ActorTolerance;
	FVector ItemLocation = GetActorLocation();
	FVector NewPosition = FMath::VInterpTo(ItemLocation, PlayerLocation, GetWorld()->GetDeltaSeconds(), 5.f);

	const float Tolerance = 20.f;
	if (ItemLocation.Equals(PlayerLocation, Tolerance))
	{
		// 우선은 파괴
		Destroy();
	}

	SetActorLocation(NewPosition);
}

void AItem::SetCanBePulled(AActor* Player)
{
	Owner = Player;
	SetActorTickEnabled(true);
}

처음엔 SetActorTickEnabled(false); 를 사용해서 Tick 함수를 꺼두고,

if (bHasOverlap)
{
	for (auto& Result : OverlapResults)
	{
		AItem* Item = Cast<AItem>(Result.GetActor());

		if (Item && !Item->GetOwner())
		{
			Item->SetCanBePulled(this);
		}
	}
}

플레이어의 FindMagneticItem() 함수에서 Item->SetCanBePulled() 함수를 호출하면 Tick이 돌면서 BeDrawnToPlayer() 함수를 계속 호출한다. 이 함수 내에서 아까 삭제한 MagneticPull() 로직을 사용한다.

우선은 가까이 다가가면 Destroy되지만, 추후 인벤토리 기능을 구현하면 다시 Owner의 아이템 관련 함수를 호출해주고 Tick을 다시 꺼주려고 생각중이다.

이렇게 개선함으로써, 플레이어가 갑자기 멀리 이동해서 탐색 범위가 아이템 밖으로 벗어나버렸을 경우 따라오던 아이템이 가만히 멈추는 현상도 해결할 수 있었다.

당연히 아이템에서 처리해야할 것 같은 기능이었는데 급하게 구현한다고 플레이어에 넣어버려서 계속 찝찝한 느낌이 들었었는데, 뭔가 속이 시원해졌다(!)

이건 코드를 구현하면서 했던 고민들. 구현하면서 벨로그를 작성하고는 있지만, 고민의 흐름을 어떻게 표현할까 계속 고민하던 차에 이런식으로 정리하는게 좋을 것 같다는 생각이 들어 정리하기 시작했다. 이번 포스팅 이후로는 이런 것도(?) 틈틈히 첨부해볼까 한다!

Jump 함수 override

void ATrapperPlayer::Jump(const FInputActionValue& Value)
{
	if (bIsMagneticMoving)
	{
		bIsMagneticMoving = false;
		ElapsedTime = 0.0f;
	}

	// Super
	bPressedJump = true;
	JumpKeyHoldTime = 0.0f;
}

당연히 매개변수가 FInputActionValue& Value 일거라고 생각했다.. 왜 Jump 함수가 override가 안되지?.... 하고 Super 함수 구현부만 복붙해놨던 과거의 나야...

UFUNCTION(BlueprintCallable, Category=Character)
ENGINE_API virtual void Jump();

캐릭터의 Jump에는 매개변수가 없더라..ㅎㅎ

void ATrapperPlayer::Jump()
{
	if (bIsMagneticMoving)
	{
		bIsMagneticMoving = false;
		ElapsedTime = 0.0f;
	}

	Super::Jump();
}

바보짓 한걸 고쳐주었다 ^-^

TurnInterp() 함수 진행 도중 Look 함수 막기

카메라 고정을 해제했을 때 카메라가 원래의 위치로 돌아가는 함수인 TurnInterp()가 동작할때(bArmRotationInterpEnd 값이 false일 때) Look함수에서 얼리 return하도록 변경했다. 크게 문제가 있던 부분은 아니었지만, 미리 잡아두는 편이 좋을 것 같아 수정했다.

void ATrapperPlayer::Look(const FInputActionValue& Value)
{
	if(!bArmRotationInterpEnd) return;

	FVector2D Data = Value.Get<FVector2D>();

	AddControllerPitchInput(Data.Y);
	AddControllerYawInput(Data.X);
}

몽타주 섹션이 넘어가지 않았던 이유

문제는 '순서' 에 있었다.

AnimInstance->Montage_JumpToSection(FName("LeftTurn"), TurnAnimationMontage);
AnimInstance->Montage_Play(TurnAnimationMontage, 1.0);

나는 당연하게도 섹션을 먼저 넘기고 몽타주를 재생시켜야 할 거라고 생각했었는데, 막상 Montage_JumpToSection 함수에 디버깅을 찍어보니

void UAnimInstance::Montage_JumpToSection(FName SectionName, const UAnimMontage* Montage)
{
	if (Montage)
	{
		FAnimMontageInstance* MontageInstance = GetActiveInstanceForMontage(Montage);
		if (MontageInstance)
		{
			bool const bEndOfSection = (MontageInstance->GetPlayRate() < 0.f);
			MontageInstance->JumpToSectionName(SectionName, bEndOfSection);
		}
	}
    else
    {  //.. 생략
}

MontageInstance가 자꾸 없다고 떴다. 그래서 Montage_Play() 함수를 찾아보니, 플레이 함수 내부에서 인스턴스를 넣어준다는 것을 알게됐다. 순서를 뒤집어주니 너무 잘 실행되더라!!.. 역시 문제를 해결하려면 코드를 읽어야 해..

캐릭터 컨트롤 옵션 관련

활을 구현한 친구와 캐릭터 코드를 합치고 보니, 캐릭터의 상태에 따라 컨트롤 데이터를 변경해주는 부분이 여기저기에 있게 되는 문제가 생겼다. 캐릭터에서 관리해야 할지, 따로 관리해주는 무언가가 필요할지 같이 논의해봐야 할 것 같다.


캐릭터의 컨트롤 옵션은 캐릭터에서 관리하는게 맞다고 판단했고, 친구가 활 관련된 코드 리팩토링 진행 후에 내가 맡아서 진행하기로 했다. 그 전에, 지금은 Tick() 함수에서 계속 돌고있는 컨트롤타입 체크 함수를 필요할때만 호출하도록 변경하기로 했다.

bActorRotationInterpEnd 변수를 bCanCameraTurn 로 변경했다.

void ATrapperPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	BowCameraTimeLine.TickTimeline(DeltaTime);

	if (bCanCameraTurn)
	{
		TurnInterp();
	}

	FindMagneticPillar();
	MagneticMove();
	FindMagneticItem();
}

Tick() 함수에서는 이 변수가 true일 경우 카메라 회전만 담당하도록 했다. 컨트롤 옵션이 변경될 때는 입력 함수들이 호출될때이므로, Look, Move, CameraFix 관련 함수 에서 CharacterControlTypeCheck() 함수를 호출하도록 했다.

void ATrapperPlayer::CameraFixStart(const FInputActionValue& Value)
{
	UE_LOG(LogTemp, Warning, TEXT("Camera Fix Start"));
	bIsAltPressed = true;

	CharacterControlTypeCheck();
}

void ATrapperPlayer::CameraFixEnd(const FInputActionValue& Value)
{
	UE_LOG(LogTemp, Warning, TEXT("Camera Fix End"));
	bIsAltPressed = false;
	bCanCameraTurn = true;

	CharacterControlTypeCheck();
}

void ATrapperPlayer::SetCharacterControlData()
{
	switch (ControlState)
	{
	case ECharacterControlType::Moving:
		if(!bCanCameraTurn) bUseControllerRotationYaw = true;
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		UE_LOG(LogTemp, Warning, TEXT("[Moving]"));
		break;
	case ECharacterControlType::MovingAlt:
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		UE_LOG(LogTemp, Warning, TEXT("[MovingAlt]"));
		break;
	case ECharacterControlType::Idle:
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		UE_LOG(LogTemp, Warning, TEXT("[Idle]"));
		break;
	case ECharacterControlType::IdleAlt:
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		UE_LOG(LogTemp, Warning, TEXT("[IdleAlt]"));
		break;
	default:
		break;
	}
}

void ATrapperPlayer::TurnInterp()
{
	FRotator SoketRelativeRotation = SpringArm->GetSocketTransform(USpringArmComponent::SocketName, RTS_Component).Rotator();
	SoketRelativeRotation.Normalize();

	float SmoothYawInput = SoketRelativeRotation.Yaw * TurnSmoothFactor;

	const float Tolerance = 0.1f;
	if (FMath::IsNearlyEqual(SoketRelativeRotation.Yaw, 0.f, Tolerance))
	{
		bCanCameraTurn = false;
		CharacterControlTypeCheck();
	}
	else
	{
		AddControllerYawInput(-SmoothYawInput);
	}
}

TurnInterp() 함수가 진행되는 과정을 간략하게 설명하면 이렇다. CameraFixEnd() 가 호출될 경우(카메라 고정키를 뗀 경우), bCanCameraTurn = true; 가 되면서 Tick() 함수에서 TurnInterp() 함수가 호출된다. 소켓의 Yaw 회전값이 0에 가까워지면 다시 변수를 false로 변경하고, 컨트롤타입 체크를 진행한다.

Moving중에 키입력을 그만두고 Idle로 변경될 경우 즉각적으로 Idle 상태로 변하진 않지만, 마우스로 카메라를 돌리는 순간 Idle로 돌아오므로 크게 문제는 없는 듯 하다.

아웃라인 버그 해결

Mesh->bRenderCustomDepth 변수에 그냥 대입하는 것 말고, SetRenderCustomDepth 함수를 사용했더니 그냥 해결됐다.. 그래도 헤매면서 다이나믹 머테리얼 인스턴스까지 다녀왔으니 만족..!!

void UPrimitiveComponent::SetRenderCustomDepth(bool bValue)
{
	if( bRenderCustomDepth != bValue )
	{
		bRenderCustomDepth = bValue;
		MarkRenderStateDirty();
	}
}

Value 값만 true로 바뀐다고 작동하는게 아니었나보다. 렌더 상태의 변경을 알리는 함수가 들어가있었다.

포스트 프로세스 볼륨을 레벨에 넣어주고, Enabled와 Unbound를 true로 설정한 뒤

void AMagneticPillar::SetOutline(bool Value)
{
	Mesh->SetRenderCustomDepth(Value);
}

// Called when the game starts or when spawned
void AMagneticPillar::BeginPlay()
{
	Super::BeginPlay();
	Mesh->SetRenderCustomDepth(false);
}

각 메시의 SetRenderCustomDepth() 를 호출해주면 정상적으로 동작한다. 그리고 BeginPlay() 함수에서 꼭 처음엔 전부 비활성화 해줘야 함! 생성자에서 하면 제대로 동작하지 않는 것 같다.

자성이동 버그 해결

아웃라인 버그를 해결하던 도중 알게된 버그가 있다. 자성이동 중에 다른 자성기둥에 포커싱하면 거기로 순간이동 해버림...! 자성이동 중엔 탐색을 중지하도록 해결해주었다.

void ATrapperPlayer::FindMagneticPillar()
{
	if (bIsMagneticMoving)
	{
		return;
	}
    
    // 생략...
}

앞으로 해야 할 것들

  • 캐릭터에서 기획자분들이 조절할 수치들을 데이터 에셋으로 빼기

  • Idle 상태에서 카메라를 회전하면 카메라를 따라 캐릭터가 회전하는 애니메이션 구현.

  • 자성 이동 중 점프 관련된 것들

  • 네트워크 공부

  • 서버-클라이언트간의 통신을 정리, 도식화

  • 2인 멀티플레이 구현

0개의 댓글