UE5 - Project/팀 프로젝트 - FPS Shooter

[UE5] - 미니맵 내 특정 액터에 대한 아이콘 렌더링

KimGeon-U 2025. 2. 25. 20:32

실행 결과

 

 

MVVM패턴 / C++  과정

  • 델리게이트를 사용하여 액터 클래스 / 거리를 건내주는 ActorComponent 기반의 클래스
  • ActorComponent를 부착할 플레이어/몬스터 클래스
  • ViewModel 역할의 MainWidget 클래스 (UUserWidget)
  • View 역할의 MininapWidget 클래스 (UUserWidget)
  • Minimap 내 배치할 MinimapIcon 클래스 (UUserWidget)

※ 미니맵에 현재 LandScape를 보여주기 위해서는 Player에게 카메라를 부착, RenderTarget을 사용한 Texture를 사용.

 

MinimapTracker (ActorComponent)

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnActorLocation, ACharacter* ,Target, float, Distance);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class GUNFIREPARAGON_API UMinimapTracker : public UActorComponent
{
	GENERATED_BODY()
protected:
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

private:
	FVector LastLocation;
	void CheckLocationChange(float DeltaSeconds, FVector OldLocation, FVector OldVelocity);

	UFUNCTION()
	void OnOwnerMoved(float DeltaSeconds, FVector OldLocation, FVector OldVelocity);
		
public:
	static FOnActorLocation OnActorLocation;
};

void UMinimapTracker::BeginPlay()
{
	Super::BeginPlay();

	ACharacter* Character = Cast<ACharacter>(GetOwner());
	if (Character)
	{
		LastLocation = Character->GetActorLocation();

		Character->OnCharacterMovementUpdated.AddDynamic(this, &UMinimapTracker::OnOwnerMoved);
	}
}

void UMinimapTracker::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	ACharacter* Character = Cast<ACharacter>(GetOwner());
	if (Character)
	{
		Character->OnCharacterMovementUpdated.RemoveDynamic(this, &UMinimapTracker::OnOwnerMoved);
	}
}

void UMinimapTracker::CheckLocationChange(float DeltaSeconds, FVector OldLocation, FVector OldVelocity)
{
	ACharacter* OwnerCharacter = Cast<ACharacter>(GetOwner());
	ACharacter* PlayerCharacter = Cast<ACharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));

	if (!OwnerCharacter || !PlayerCharacter) return;

	FVector CurrentLocation = OwnerCharacter->GetActorLocation();
	float Distance = FVector::Dist(CurrentLocation, PlayerCharacter->GetActorLocation());

	if (!CurrentLocation.Equals(LastLocation, 5.0f))
	{
		OnActorLocation.Broadcast(OwnerCharacter, Distance);
		LastLocation = CurrentLocation;
	}
}

void UMinimapTracker::OnOwnerMoved(float DeltaSeconds, FVector OldLocation, FVector OldVelocity)
{
	CheckLocationChange(DeltaSeconds, OldLocation, OldVelocity);
}

 

FOnActorLocation Delegate

  • Target : 미니맵 아이콘을 적용할 대상(Actor Component)를 가지고 있는 Owner를 전송
  • Dist : 게임 프로젝트 기획 시 싱글프로젝트 기반. PlayerController Index 0 빙의 캐릭터와의 거리를 계산

OnCharacterMovementUpdated

  • Character 내 구현되어있는 델리게이트 시그니처. DeltaSecond, OldLocation, OldVelocity 반환
  • 매 Tick마다 전송을 하는것이 아닌, 캐릭터가 이동했을 경우 데이터를 전송함으로써 최적화 하기 위해 사용

OnOwnerMoved / CheckLocationChange

  • 이벤트 바인딩 및 필터링 / OnActorLocation 내 BroadCast하기 전 계산 로직
  • OnOwnerMoved는 이벤트 바인딩 역할하여 전송만 하지만, SRP 및 확장성을 고려하여 분리

현재 로직의 문제점

  • 캐릭터가 움직임을 감지했을 경우 (OnCharacterMovementUpdated)가 호출된 경우만 값이 변경된다.
  • 초기 생성 위치를 전송하지 않는다 / Character가 소멸될 경우에만 제거된다.
    •  BeginPlay시 BroadCast 추가 / Character isHidden() == true 시 BroadCast.

 

MainWidget

UFUNCTION()
void OnMinimapUpdated(ACharacter* Target, float Distance);

// Minimap Rendering Character Distance
float MaxRenderDistance = 1600.f;

void UUserWidget::OnMinimapUpdated(ACharacter* Target, float Distance)
{
	if (Distance <= MaxRenderDistance)
	{
		if (!MinimapWidget->ActiveIcons.Contains(Target))
		{
			MinimapWidget->AddMinimapIcon(Target);
		}

		MinimapWidget->UpdateActorIcon(Target, Target->GetActorLocation());
	}
	else
	{
		MinimapWidget->RemoveMinimapIcon(Target);
	}
}
  • ActorComponent에서 BroadCast한 데이터들을 Minimap에 전송하는 단계.
  • 델리게이트 시그니처를 바인딩하여 해당 함수를 통해 Model 데이터들을 View계층에 전달한다.

 

MinimapIcon

UCLASS()
class GUNFIREPARAGON_API UIngameMinimapIcon : public UUserWidget
{
	GENERATED_BODY()
	
public:	
	UFUNCTION()
	void SetIconTexture(UTexture2D* Texture);
	
	UFUNCTION()
	void SetVisibilityBasedOnDistance(float Distance, float MaxDistance);

protected:
	UPROPERTY(meta = (BindWidget))
	class UImage* IconImage;
};

void UIngameMinimapIcon::SetIconTexture(UTexture2D* Texture)
{
	if (IconImage)
	{
		IconImage->SetBrushFromTexture(Texture, true);
	}
}

void UIngameMinimapIcon::SetVisibilityBasedOnDistance(float Distance, float MaxDistance)
{
	SetVisibility(Distance > MaxDistance ? ESlateVisibility::Hidden : ESlateVisibility::Visible);
}

 

미니맵에 배치할 아이콘에 대한 위젯.

  • SetIconTexture - 동적으로 액터에 적합한 텍스처를 변경하기 위함
  • SetVisibilityBasedOnDistance - 플레이어와의 거리에 따른 아이콘 상태 변경

 

Minimap.cpp

// 전송받은 Target에게 어떤 아이콘을 입힐지, 디자이너 패널 내 어디에 배치할지 결정하는 로직
void UInGameMinimap::AddMinimapIcon(ACharacter* Target)
{
	if (!Target || ActiveIcons.Contains(Target) || !MinimapIconClass) return;

	UIngameMinimapIcon* NewIcon = CreateWidget<UIngameMinimapIcon>(this, MinimapIconClass);
	if (NewIcon)
	{
		RenderCanvas->AddChild(NewIcon);
		ActiveIcons.Add(Target, NewIcon);

		if (Target == PlayerCharacter)
		{
			NewIcon->SetIconTexture(PlayerIconTexture);
		}
		else
		{
			NewIcon->SetIconTexture(EnemyIconTexture);
		}
		int32 ChildCount = RenderCanvas->GetChildrenCount();

		UpdateActorIcon(Target, Target->GetActorLocation());
	}
}

// ICon 삭제에 관련된 로직
void UInGameMinimap::RemoveMinimapIcon(ACharacter* Target)
{
	if (UIngameMinimapIcon** IconPtr = ActiveIcons.Find(Target))
	{
		if (UIngameMinimapIcon* Icon = *IconPtr)
		{
			Icon->RemoveFromParent();
		}
		ActiveIcons.Remove(Target);
	}
}

// 특정 타겟의 거리계산 및 아이콘 배치 위치, 렌더링 여부
void UInGameMinimap::UpdateActorIcon(ACharacter* RenderTarget, FVector WorldLocation)
{
	if (!RenderTarget || !RenderCanvas) return;

	FVector2D MinimapPosition = IconRenderPosition(WorldLocation);

	if (ActiveIcons.Contains(RenderTarget))
	{
		UIngameMinimapIcon* Icon = ActiveIcons[RenderTarget];

		Icon->SetRenderTranslation(MinimapPosition);
		float Distance = FVector::Dist(PlayerCharacter->GetActorLocation(), WorldLocation);
		Icon->SetVisibilityBasedOnDistance(Distance, MaxRenderDistance);
	}
}

// 플레이어의 전방벡터를 받아와, 미니맵에 렌더링되는 방향을 계산한다.
FVector2D UInGameMinimap::IconRenderPosition(FVector WorldLocation)
{
	if (!PlayerCharacter || !RenderCanvas) return FVector2D::ZeroVector;

	FVector Offset = WorldLocation - PlayerCharacter->GetActorLocation();
	Offset *= MinimapScale;

	FVector Forward = PlayerCharacter->GetActorForwardVector(); 
	FVector Right = PlayerCharacter->GetActorRightVector(); 

	float RotatedX = FVector::DotProduct(Offset, Forward);
	float RotatedY = FVector::DotProduct(Offset, Right);

	FVector2D MinimapCenter = FVector2D(RenderCanvas->GetCachedGeometry().GetLocalSize()) * 0.5f;
	FVector2D MinimapPosition = MinimapCenter + FVector2D(RotatedY, -RotatedX);

	UE_LOG(LogTemp, Warning, TEXT("WorldLocation: (%.1f, %.1f) -> MinimapPosition: (%.1f, %.1f)"),
		WorldLocation.X, WorldLocation.Y, MinimapPosition.X, MinimapPosition.Y);

	return MinimapPosition;
}

 

 

※ 현재 로직은 하드코딩된 상태입니다. (확장성을 고려하지 않은상태)

 

UpdateActorIcon

  • 파라미터 타겟에 대한 미니맵과 뷰포트간 실시간 동기화를 위한 로직
  • 해당 로직을 ActiveIcons에 삽입되어있는 Target들을 매 Tick마다 이동값을 계산하여 정합성을 만족한다.

Add/RemoveIcon

  • ActorComponent에 대한 값을 전달받은 후 어떤 텍스처를 입혀줄지 결정하는 단계 / 제거하는 단계
  • 플레이어 / 그외 플레이어(몬스터) 에 대한 렌더링만을 담당한다.
  • 그 외 액터를 추가하여 텍스처를 결정하고 싶은 경우 DataAsset/Table를 활용한 Contains / IsA 검증 후 텍스처 결정.

IconRenderPosition

  • 플레이어를 미니맵 내 정중앙 배치. (MinimapCenter)
  • 플레이어의 전방벡터와 거리를 계산하여 렌더링 할 다른 캐릭터의 위치값 계산, 변환
    • WorldLocation (FVector 3D) -> WidgetLocation (FVector 2D)

 

https://www.youtube.com/watch?v=vBKFwI_JZpE

 

 

출처 및 학습자료


https://www.youtube.com/watch?v=xWbJiCSwxSE&list=PLNTm9yU0zou6xdEqL2QSSfanaZ3KA_vU7&index=4