Bounding Widget Component 구현

Woogle·3일 전
0

언리얼 엔진 5

목록 보기
63/63

📄 개요

액터의 Bounding Box를 기반으로 영역이 맞춰지는 위젯 컴포넌트를 만들었다.
이것으로 게임 화면에서 직접 3D 모델링을 클릭하거나 터치할 수 있다!


📄 구현 과정

✏️ 1단계: Bounding Box의 꼭지점 구하기

  • 위젯 스페이스는 EWidgetSpace::Screen으로 설정한다.
  • 매 Tick마다 대상의 Bounding Box 꼭지점 위치를 구한다.
UBoundingWidgetComponent::UBoundingWidgetComponent()
{
	PrimaryComponentTick.bCanEverTick = true;	
	SetWidgetSpace(EWidgetSpace::Screen);
}

void UBoundingWidgetComponent::UpdateBoxVertices(float DeltaTime)
{
    if (GetAttachParent())
    {
        FBox ParentBox = GetAttachParent()->Bounds.GetBox();
        ParentBox.GetVertices(BoxVertices);
    }
    break;

    // 디버그용 꼭지점 시각화
    for (FVector Verticle : BoxVertices)
    {
        DrawDebugPoint(GetWorld(), Verticle, 10.f, FColor::Red, false, DeltaTime);
    }
}

✏️ 2단계: 위젯 크기 조절

  • 각 꼭지점 위치를 화면 좌표로 변환하고, 최대/최소값을 계산해 위젯의 크기를 설정한다.
void UBoundingWidgetComponent::UpdateWidgetSize(float DeltaTime)
{
    FVector2D ScreenMin(FLT_MAX, FLT_MAX);
    FVector2D ScreenMax(-FLT_MAX, -FLT_MAX);

    if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
    {
        // Box Vertices 화면 좌표 변환
        for (const FVector& Vertex : BoxVertices)
        {
            FVector2D ScreenPos;
            if (PlayerController->ProjectWorldLocationToScreen(Vertex, ScreenPos))
            {
                FGeometry Geometry = UWidgetLayoutLibrary::GetPlayerScreenWidgetGeometry(PlayerController);
                FVector2D LocalToAbsoluteScale = Geometry.GetLocalSize() / Geometry.GetAbsoluteSize();
                ScreenPos *= LocalToAbsoluteScale;

                ScreenMin.X = FMath::Min(ScreenMin.X, ScreenPos.X);
                ScreenMin.Y = FMath::Min(ScreenMin.Y, ScreenPos.Y);
                ScreenMax.X = FMath::Max(ScreenMax.X, ScreenPos.X);
                ScreenMax.Y = FMath::Max(ScreenMax.Y, ScreenPos.Y);
            }
        }

        // 위젯 크기 설정
        SetDrawSize(ScreenMax - ScreenMin);
    }
}

✏️ 3단계: 위젯 위치 조절

  • 위젯 컴포넌트 위치를 Bounding Box 중심으로 옮겨서 위치를 맞춘다.
void UBoundingWidgetComponent::UpdateWidgetPosition(float DeltaTime)
{
    FVector BoxOrigin = FVector::ZeroVector;
    for (const FVector& Vertex : BoxVertices)
    {
        BoxOrigin += Vertex;
    }
    BoxOrigin /= 8;

    SetWorldLocation(BoxOrigin);
}

📄 완성된 코드

  • 바운딩 박스 대상을 Root Component, Attached Component, All Meshes 중에 선택할 수 있게 개선했다.
  • 위젯 Scale도 조정할 수 있게 개선했다.

🚀 BoundingWidgetComponent.h

UENUM(BlueprintType)
enum class EBoundingWidgetTarget : uint8
{
	AttachedComponent,
	RootComponent,
	AllMeshes
};

UCLASS(Blueprintable, meta=(BlueprintSpawnableComponent))
class UBoundingWidgetComponent : public UWidgetComponent
{
	GENERATED_BODY()

public:
	UBoundingWidgetComponent();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	EBoundingWidgetTarget BoundingTarget = EBoundingWidgetTarget::RootComponent;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FVector2D WidgetScale = FVector2D(1.f, 1.f);

public:
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

private:
	UPROPERTY()
	FVector BoxVertices[8];

	void UpdateBoxVertices(float DeltaTime);
	void UpdateWidgetSize(float DeltaTime);
	void UpdateWidgetPosition(float DeltaTime);
};

🚀 BoundingWidgetComponent.cpp

UBoundingWidgetComponent::UBoundingWidgetComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
	SetWidgetSpace(EWidgetSpace::Screen);
}

void UBoundingWidgetComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    UpdateBoxVertices(DeltaTime);
    UpdateWidgetSize(DeltaTime);
    UpdateWidgetPosition(DeltaTime);
}

void UBoundingWidgetComponent::UpdateBoxVertices(float DeltaTime)
{
    switch (BoundingTarget)
    {
    case EBoundingWidgetTarget::RootComponent:
        if (GetOwner() && GetOwner()->GetRootComponent())
        {
            FBox RootBox = GetOwner()->GetRootComponent()->Bounds.GetBox();
            RootBox.GetVertices(BoxVertices);
        }
        break;

    case EBoundingWidgetTarget::AttachedComponent:
        if (GetAttachParent())
        {
            FBox ParentBox = GetAttachParent()->Bounds.GetBox();
            ParentBox.GetVertices(BoxVertices);
        }
        break;

    case EBoundingWidgetTarget::AllMeshes:
        if (GetOwner())
        {
            FVector Origin;
            FVector Extent;
            GetOwner()->GetActorBounds(true, Origin, Extent);
            BoxVertices[0] = Origin + FVector(Extent.X, Extent.Y, Extent.Z);
            BoxVertices[1] = Origin + FVector(-Extent.X, Extent.Y, Extent.Z);
            BoxVertices[2] = Origin + FVector(Extent.X, -Extent.Y, Extent.Z);
            BoxVertices[3] = Origin + FVector(-Extent.X, -Extent.Y, Extent.Z);
            BoxVertices[4] = Origin + FVector(Extent.X, Extent.Y, -Extent.Z);
            BoxVertices[5] = Origin + FVector(-Extent.X, Extent.Y, -Extent.Z);
            BoxVertices[6] = Origin + FVector(Extent.X, -Extent.Y, -Extent.Z);
            BoxVertices[7] = Origin + FVector(-Extent.X, -Extent.Y, -Extent.Z);
        }
        break;
    }
}

void UBoundingWidgetComponent::UpdateWidgetSize(float DeltaTime)
{
    FVector2D ScreenMin(FLT_MAX, FLT_MAX);
    FVector2D ScreenMax(-FLT_MAX, -FLT_MAX);

    if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
    {
        for (const FVector& Vertex : BoxVertices)
        {
            FVector2D ScreenPos;
            if (PlayerController->ProjectWorldLocationToScreen(Vertex, ScreenPos))
            {
                FGeometry Geometry = UWidgetLayoutLibrary::GetPlayerScreenWidgetGeometry(PlayerController);
                FVector2D LocalToAbsoluteScale = Geometry.GetLocalSize() / Geometry.GetAbsoluteSize();
                ScreenPos *= LocalToAbsoluteScale;

                ScreenMin.X = FMath::Min(ScreenMin.X, ScreenPos.X);
                ScreenMin.Y = FMath::Min(ScreenMin.Y, ScreenPos.Y);
                ScreenMax.X = FMath::Max(ScreenMax.X, ScreenPos.X);
                ScreenMax.Y = FMath::Max(ScreenMax.Y, ScreenPos.Y);
            }
        }

        float WidgetSizeX = (ScreenMax - ScreenMin).X * WidgetScale.X;
        float WidgetSizeY = (ScreenMax - ScreenMin).Y * WidgetScale.Y;
        SetDrawSize(FVector2D(WidgetSizeX, WidgetSizeY));
    }
}

void UBoundingWidgetComponent::UpdateWidgetPosition(float DeltaTime)
{
    FVector BoxOrigin = FVector::ZeroVector;
    for (const FVector& Vertex : BoxVertices)
    {
        BoxOrigin += Vertex;
    }
    BoxOrigin /= 8;

    SetWorldLocation(BoxOrigin);
}

📄 테스트

  • 위젯 컴포넌트 부착 대상과 BoundingTarget을 바꿔가며 테스트했다.

✏️ Root Component 모드

  • 캐릭터의 아무 곳에 붙이고 EBoundingWidgetTarget::RootComponent 사용
  • 캐릭터 캡슐을 클릭하는 용도

✏️ Attached Component 모드

  • 모자(Cone)에 붙이고 EBoundingWidgetTarget::AttachedComponent 사용
  • 캐릭터가 장착한 무기나 장비를 클릭하는 용도

✏️ All Meshes 모드

  • 캐릭터의 아무 곳에 붙이고 EBoundingWidgetTarget::AllMeshes 사용
  • 장비까지 포함한 캐릭터를 클릭하는 용도


📄 참고 자료

profile
노력하는 게임 개발자

0개의 댓글