오늘은 룬 시스템의 드래그 비주얼 위젯을 근본적으로 개선했음
기존 방식은 단일 이미지를 사용해서 복잡한 오프셋 계산으로 배치 위치를 예측하는 방식이었는데, 플레이어가 예상하는 위치와 실제 배치 위치가 달라서 UX가 좋지 않았음
새로운 방식은 드래그 비주얼 자체를 룬 모양에 맞는 그리드로 생성하고, 기준셀을 마우스 커서에 정확히 위치시켜서 "보이는 대로 배치되는" 완벽한 WYSIWYG 시스템을 구현했음

드래그 앤 드롭으로 룬을 배치할 때 사용자 경험이 좋지 않다는 피드백이 계속 들어왔음
기존 방식의 한계:
FindOptimalPlacementPos() 같은 복잡한 예측 로직 필요핵심 문제
// 복잡한 오프셋 계산 방식
if (BoardManager->FindOptimalPlacementPos(SelectedRuneID, ClickedCellPos, OptimalPos))
{
// 예측된 위치에 배치... 하지만 부정확함
}
핵심 아이디어: "룬의 기준셀을 항상 마우스 커서에 위치"
기존의 복잡한 계산 대신 직관적인 방식으로 변경:
기존: 마우스 클릭 → 예측 계산 → 배치 위치 결정
새로운: 룬의 (0,0) 셀을 마우스에 위치 → 마우스 위치 = 배치 위치
DragVisualWidget 구조 변경
// 기존: 단일 이미지
UImage* RuneImage;
// 새로운: 룬 모양 그리드
UUniformGridPanel* RuneGridPanel;
TMap<FIntPoint, UImage*> GridCellWidgets; // 각 셀별 위젯
Setup 함수 개선
void UGS_DragVisualWidget::Setup(uint8 InRuneID, UTexture2D* InTexture,
const TMap<FIntPoint, UTexture2D*>& RuneShape, const FVector2D& CellSize)
{
// 룬 모양에 맞는 그리드 생성
CreateRuneShapeGrid(RuneShape);
// 기준셀(0,0)의 위치 계산
CalculateReferenceCellOffset();
}
그리드 생성 핵심 로직
void UGS_DragVisualWidget::CreateRuneShapeGrid(const TMap<FIntPoint, UTexture2D*>& RuneShape)
{
for (const auto& ShapePair : RuneShape)
{
FIntPoint CellPos = ShapePair.Key;
UTexture2D* CellTexture = ShapePair.Value;
// 각 셀마다 이미지 위젯 생성
UImage* CellImage = NewObject<UImage>(this);
CellImage->SetBrushFromTexture(CellTexture);
CellImage->SetDesiredSizeOverride(TargetCellSize); // 아케인보드와 동일 크기
// 그리드에 배치
FIntPoint GridPos = CellPos - MinPos;
RuneGridPanel->AddChildToUniformGrid(CellImage, GridPos.X, GridPos.Y);
}
}
X, Y 좌표 순서 문제
// 잘못된 순서 (회전된 모양으로 표시됨)
RuneGridPanel->AddChildToUniformGrid(CellImage, GridPos.Y, GridPos.X);
// 올바른 순서
RuneGridPanel->AddChildToUniformGrid(CellImage, GridPos.X, GridPos.Y);
AddChildToUniformGrid(Widget, Row, Column) 함수의 매개변수 순서를 제대로 파악하지 못해서 룬이 회전되어 보이는 문제가 있었음
HitTestInvisible로 해결
// 문제: 드래그 비주얼이 마우스 이벤트를 차단
SelectionVisualWidget->SetVisibility(ESlateVisibility::Visible); // ❌
// 해결: 보이지만 마우스 이벤트는 통과
SelectionVisualWidget->SetVisibility(ESlateVisibility::HitTestInvisible); // ✅
드래그 비주얼 위에 마우스가 올라가면 NativeOnMouseMove가 호출되지 않아서 드래그 비주얼이 멈춰있는 문제를 발견하고 해결함
핵심 문제 발견
// StartRuneSelection에서
FVector2D MousePos = UWidgetLayoutLibrary::GetMousePositionOnViewport(GetWorld());
SelectionVisualWidget->SetPositionInViewport(AdjustedPos, false);
// NativeOnMouseMove에서
FVector2D MousePos = InMouseEvent.GetScreenSpacePosition();
FVector2D ViewportMousePos = ScreenGeometry.AbsoluteToLocal(MousePos);
SelectionVisualWidget->SetPositionInViewport(AdjustedPos, false);
문제점
StartRuneSelection: 뷰포트 좌표계 사용NativeOnMouseMove: 스크린 좌표를 뷰포트로 변환해서 사용일관된 좌표계 사용
void UGS_ArcaneBoardWidget::PositionDragVisualAtMouse()
{
// 항상 뷰포트 좌표 사용으로 통일
FVector2D MousePos = UWidgetLayoutLibrary::GetMousePositionOnViewport(GetWorld());
FVector2D ReferenceCellOffset = SelectionVisualWidget->GetReferenceCellOffset();
FVector2D AdjustedPos = MousePos - ReferenceCellOffset;
SelectionVisualWidget->SetPositionInViewport(AdjustedPos, false);
}
MouseMove에서도 동일한 함수 호출
FReply UGS_ArcaneBoardWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (bIsInSelectionMode && SelectionVisualWidget)
{
PositionDragVisualAtMouse(); // 통일된 함수 사용
// 마우스 아래 셀 = 배치 위치 (계산 불필요!)
UGS_RuneGridCellWidget* CellUnderMouse = GetCellAtPos(MousePos);
if (CellUnderMouse)
{
UpdateGridPreview(SelectedRuneID, CellUnderMouse->GetCellPos());
}
}
}

UX 개선 결과
FindOptimalPlacementPos 같은 복잡한 로직 제거배치 로직 단순화
// 기존: 복잡한 예측 계산
FIntPoint OptimalPos;
if (BoardManager->FindOptimalPlacementPos(SelectedRuneID, ClickedCellPos, OptimalPos))
// 새로운: 단순한 직접 배치
FIntPoint PlacementPos = TargetCell->GetCellPos(); // 마우스 위치 = 배치 위치
if (BoardManager->CanPlaceRuneAt(SelectedRuneID, PlacementPos))
1. WYSIWYG (What You See Is What You Get)
- 드래그 중 보이는 위치 = 실제 배치 위치
2. 일관된 좌표계 사용
- 모든 위치 계산에서 동일한 좌표 기준점 유지
3. 기준점 명확화
- 복잡한 오프셋 계산보다는 명확한 기준점 설정
4. 시각적 피드백
- 실시간 미리보기로 배치 가능 여부 표시
// 좌표 순서 주의
AddChildToUniformGrid(Widget, Row, Column)
// Row = Y축 (세로), Column = X축 (가로)
// 셀 크기 통일
CellImage->SetDesiredSizeOverride(TargetSize);
// 정렬 설정
ImageSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
ImageSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill);