실행 결과
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
'UE5 - Project > 팀 프로젝트 - FPS Shooter' 카테고리의 다른 글
[UE5] - 팀프로젝트 KPT 회고 및 개인 피드백 (0) | 2025.03.07 |
---|---|
[UE5] - 팀 프로젝트 UI 구현 목록 (1) | 2025.03.06 |
[UE Plugin] - ASync Loading Screen Plugin을 사용한 로딩 스크린 (0) | 2025.03.04 |
[UE5] - UI 관련 MVVM 기반의 클래스 구조 설계 (0) | 2025.02.20 |
[UE5] - FPS 싱글플레이 슈터 프로젝트 기획 (0) | 2025.02.17 |