레이더 차트의 기본 개념과 UMG 적용 방안
Unreal Engine에서 UMG를 사용하여 데이터 시각화 요소를 제작할 때, 종종 차트 관련 기능이 부족하다는 점을 느낄 수 있습니다. 특히 레이더 차트(Radar Chart)처럼 복수의 축에 따라 데이터를 표현해야 하는 경우, 기본 제공 위젯만으로는 한계가 있습니다. 하지만 UUserWidget의 NativePaint 메서드를 오버라이드하면 Slate 렌더링 시스템을 직접 활용해 커스텀 도형을 그릴 수 있습니다.
핵심 설계: 삼각형 기반 폴리곤 그리기
레이더 차트는 각 카테고리별 데이터 값을 반경 방향으로 표현하며, 중심점에서부터 해당 각도 방향으로 뻗은 점들을 연결해 다각형 형태로 시각화합니다. 이 과정에서 각 섹션은 중심점과 두 인접한 데이터 점 사이의 삼각형으로 구성됩니다. 따라서 복잡한 곡선 대신 단순한 직선 기반 폴리곤을 사용함으로써 성능과 정확성을 동시에 확보할 수 있습니다.
헤더 파일 정의
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RadarChartWidget.generated.h"
UCLASS(Blueprintable)
class PROJECTNAME_API URadarChartWidget : public UUserWidget
{
GENERATED_BODY()
protected:
virtual int32 NativePaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const override;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
float Radius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
TArray<float> DataValues;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
TArray<FColor> SegmentColors;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
TArray<float> Angles;
UFUNCTION(BlueprintCallable, Category = "Chart")
void InitializeFromPercentages(const TArray<float>& Percents);
UFUNCTION(BlueprintCallable, Category = "Chart")
void UpdateDataRadii(const TArray<float>& Radii);
private:
void DrawSegment(FSlateWindowElementList& OutDrawElements,
const FGeometry& Geometry,
int32 Layer,
float CenterX,
float CenterY,
float StartAngle,
float EndAngle,
float StartRadius,
float EndRadius,
const FColor& Tint) const;
FColor RetrieveSegmentColor(int32 Index) const;
float GetDataRadius(int32 Index) const;
};
구현 코드 분석
#include "RadarChartWidget.h"
#include "Rendering/SlateRenderer.h"
int32 URadarChartWidget::NativePaint(const FPaintArgs& Args,
const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements,
int32 LayerId,
const FWidgetStyle& InWidgetStyle,
bool bParentEnabled) const
{
if (Angles.Num() < 2 || DataValues.Num() == 0) return LayerId;
float accumulatedAngle = 0.f;
float currentRadius, nextRadius;
float centerX = Radius;
float centerY = Radius;
for (int32 i = 0; i < Angles.Num() - 1; ++i)
{
float startAngle = accumulatedAngle + Angles[i];
float endAngle = startAngle + Angles[i + 1];
currentRadius = GetDataRadius(i);
nextRadius = GetDataRadius(i + 1);
FColor segmentColor = RetrieveSegmentColor(i);
DrawSegment(OutDrawElements, AllottedGeometry, LayerId,
centerX, centerY,
startAngle, endAngle,
currentRadius, nextRadius,
segmentColor);
accumulatedAngle = startAngle;
}
return LayerId + 1;
}
void URadarChartWidget::DrawSegment(FSlateWindowElementList& OutDrawElements,
const FGeometry& Geometry,
int32 Layer,
float CenterX,
float CenterY,
float StartAngle,
float EndAngle,
float StartRadius,
float EndRadius,
const FColor& Color) const
{
if (StartAngle >= EndAngle) return;
TArray<FSlateVertex> Vertices;
TArray<SlateIndex> Indices;
FVector2D centerScreen(CenterX, CenterY);
FVector2D startPoint(
CenterX + StartRadius * FMath::Cos(FMath::DegreesToRadians(StartAngle - 90)),
CenterY + StartRadius * FMath::Sin(FMath::DegreesToRadians(StartAngle - 90))
);
FVector2D endPoint(
CenterX + EndRadius * FMath::Cos(FMath::DegreesToRadians(EndAngle - 90)),
CenterY + EndRadius * FMath::Sin(FMath::DegreesToRadians(EndAngle - 90))
);
const FSlateRenderTransform& transform = Geometry.ToPaintGeometry().GetAccumulatedRenderTransform();
centerScreen = transform.TransformPoint(centerScreen);
startPoint = transform.TransformPoint(startPoint);
endPoint = transform.TransformPoint(endPoint);
FSlateVertex vCenter, vStart, vEnd;
vCenter.Position = centerScreen;
vStart.Position = startPoint;
vEnd.Position = endPoint;
vCenter.Color = Color;
vStart.Color = Color;
vEnd.Color = Color;
const int32 idxCenter = Vertices.Add(vCenter);
const int32 idxStart = Vertices.Add(vStart);
const int32 idxEnd = Vertices.Add(vEnd);
Indices.Add(idxCenter);
Indices.Add(idxStart);
Indices.Add(idxEnd);
const FSlateBrush* dummyBrush = FCoreStyle::Get().GetBrush("WhiteTexture");
const FSlateResourceHandle resource = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*dummyBrush);
FSlateDrawElement::MakeCustomVerts(
OutDrawElements,
Layer,
resource,
Vertices,
Indices,
nullptr,
0,
0
);
}
FColor URadarChartWidget::RetrieveSegmentColor(int32 Index) const
{
return SegmentColors.IsValidIndex(Index) ? SegmentColors[Index] : FColor::FromHex("#FF5555");
}
float URadarChartWidget::GetDataRadius(int32 Index) const
{
if (DataValues.IsValidIndex(Index))
return DataValues[Index] * Radius * 0.01f; // 백분율 기반 스케일링
return Radius * 0.5f;
}
void URadarChartWidget::InitializeFromPercentages(const TArray<float>& Percents)
{
Angles.Reset();
Angles.Add(0.f);
float total = 0.f;
for (float val : Percents) total += val;
for (float val : Percents)
{
Angles.Add(val / total * 360.f);
}
}
void URadarChartWidget::UpdateDataRadii(const TArray<float>& Radii)
{
DataValues = Radii;
}
블루프린트 통합 팁
C++ 클래스를 기반으로 한 위젯 블루프린트가 그래프나 디자이너 탭을 잃는 문제가 발생할 수 있습니다. 이를 해결하려면 다음과 같은 절차를 따르세요:
- 임시로 UserWidget에서 상속받는 BP 생성
- 생성 후 Class Settings에서 부모 클래스를 커스텀 C++ 클래스(
URadarChartWidget)로 변경 - 기본 캔버스 패널을 제거하고, 필요한 변수를 초기화
이 방법을 통해 블루프린트 편집 기능을 유지하면서도 커스텀 그리기 로직을 사용할 수 있습니다.
결과 및 개선 가능성
위 코드는 기본적인 레이더 차트를 실시간으로 렌더링하며, 데이터 업데이트와 색상 지정이 블루프린트에서도 가능하도록 설계되었습니다. 추가 개선 포인트로는:
- 여러 계층의 동심원 배경 그리기
- 텍스트 레이블 오버레이
- 애니메이션을 통한 값 전환 효과
- 마우스 오버 시 하이라이트 기능
등이 있습니다.