[25.06.28] :: 가디언 앤 시커 프로젝트 19

chooha·2025년 6월 28일

가디언앤시커

목록 보기
19/25

📝 개발일지 - 드래그 비주얼 위젯 UX 개선

👨💻 오늘의 개발 작업

오늘은 룬 시스템의 드래그 비주얼 위젯을 근본적으로 개선했음

기존 방식은 단일 이미지를 사용해서 복잡한 오프셋 계산으로 배치 위치를 예측하는 방식이었는데, 플레이어가 예상하는 위치와 실제 배치 위치가 달라서 UX가 좋지 않았음

새로운 방식은 드래그 비주얼 자체를 룬 모양에 맞는 그리드로 생성하고, 기준셀을 마우스 커서에 정확히 위치시켜서 "보이는 대로 배치되는" 완벽한 WYSIWYG 시스템을 구현했음


💡 오늘의 5분 기록

1. 기존 방식의 문제점 파악

드래그 앤 드롭으로 룬을 배치할 때 사용자 경험이 좋지 않다는 피드백이 계속 들어왔음

기존 방식의 한계:

  • 단일 이미지 드래그 비주얼 사용
  • FindOptimalPlacementPos() 같은 복잡한 예측 로직 필요
  • 다양한 룬 모양에 맞게 커서에 위치하게 생성하기 복잡함
  • 테두리 근처에서 정확도 떨어짐
  • 플레이어 예상 위치 ≠ 실제 배치 위치

핵심 문제

// 복잡한 오프셋 계산 방식
if (BoardManager->FindOptimalPlacementPos(SelectedRuneID, ClickedCellPos, OptimalPos))
{
    // 예측된 위치에 배치... 하지만 부정확함
}

2. 새로운 접근 방식 설계 - 기준셀 기반 시스템

핵심 아이디어: "룬의 기준셀을 항상 마우스 커서에 위치"

기존의 복잡한 계산 대신 직관적인 방식으로 변경:

기존: 마우스 클릭 → 예측 계산 → 배치 위치 결정
새로운: 룬의 (0,0) 셀을 마우스에 위치 → 마우스 위치 = 배치 위치

DragVisualWidget 구조 변경

// 기존: 단일 이미지
UImage* RuneImage;

// 새로운: 룬 모양 그리드
UUniformGridPanel* RuneGridPanel;
TMap<FIntPoint, UImage*> GridCellWidgets; // 각 셀별 위젯

3. 그리드 생성 로직 구현

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);
    }
}

4. 첫 번째 트러블슈팅 - 좌표계 혼동

X, Y 좌표 순서 문제

// 잘못된 순서 (회전된 모양으로 표시됨)
RuneGridPanel->AddChildToUniformGrid(CellImage, GridPos.Y, GridPos.X);

// 올바른 순서
RuneGridPanel->AddChildToUniformGrid(CellImage, GridPos.X, GridPos.Y);

AddChildToUniformGrid(Widget, Row, Column) 함수의 매개변수 순서를 제대로 파악하지 못해서 룬이 회전되어 보이는 문제가 있었음

5. 두 번째 트러블슈팅 - 마우스 이벤트 차단

HitTestInvisible로 해결

// 문제: 드래그 비주얼이 마우스 이벤트를 차단
SelectionVisualWidget->SetVisibility(ESlateVisibility::Visible); // ❌

// 해결: 보이지만 마우스 이벤트는 통과
SelectionVisualWidget->SetVisibility(ESlateVisibility::HitTestInvisible); // ✅

드래그 비주얼 위에 마우스가 올라가면 NativeOnMouseMove가 호출되지 않아서 드래그 비주얼이 멈춰있는 문제를 발견하고 해결함

6. 세 번째 트러블슈팅 - 좌표계 불일치 문제 ⚠️

핵심 문제 발견

// 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: 스크린 좌표를 뷰포트로 변환해서 사용
  • 결과: 드래그 시작할 때와 이동할 때 기준점이 달라짐

7. 최종 해결 - 좌표계 통일

일관된 좌표계 사용

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());
        }
    }
}

8. 성과와 개선 효과

UX 개선 결과

  • 완벽한 WYSIWYG: 보이는 대로 정확히 배치
  • 직관적 조작: 룬의 기준셀이 항상 마우스 위치에 따라감
  • 테두리 정확도: 복잡한 계산 없이 정확한 배치
  • 코드 단순화: FindOptimalPlacementPos 같은 복잡한 로직 제거

배치 로직 단순화

// 기존: 복잡한 예측 계산
FIntPoint OptimalPos;
if (BoardManager->FindOptimalPlacementPos(SelectedRuneID, ClickedCellPos, OptimalPos))

// 새로운: 단순한 직접 배치
FIntPoint PlacementPos = TargetCell->GetCellPos(); // 마우스 위치 = 배치 위치
if (BoardManager->CanPlaceRuneAt(SelectedRuneID, PlacementPos))

📚 개발 참고

드래그 앤 드롭 UX 설계 원칙

1. WYSIWYG (What You See Is What You Get)
   - 드래그 중 보이는 위치 = 실제 배치 위치

2. 일관된 좌표계 사용
   - 모든 위치 계산에서 동일한 좌표 기준점 유지

3. 기준점 명확화  
   - 복잡한 오프셋 계산보다는 명확한 기준점 설정

4. 시각적 피드백
   - 실시간 미리보기로 배치 가능 여부 표시

UniformGridPanel 활용 팁

// 좌표 순서 주의
AddChildToUniformGrid(Widget, Row, Column)
// Row = Y축 (세로), Column = X축 (가로)

// 셀 크기 통일
CellImage->SetDesiredSizeOverride(TargetSize);

// 정렬 설정
ImageSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
ImageSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill);

핵심 포인트

  • DragVisual: 단일 이미지 → 룬 모양 그리드로 변경
  • 기준셀 시스템: 룬의 (0,0) 셀을 마우스 커서에 고정
  • 좌표계 통일: StartRuneSelection과 MouseMove에서 동일한 좌표 처리
  • HitTestInvisible: 드래그 비주얼이 마우스 이벤트를 차단하지 않도록 설정
  • WYSIWYG 달성: 복잡한 예측 로직 없이 직관적인 배치 시스템 구현

0개의 댓글