[25.07.21] :: 가디언 앤 시커 프로젝트 21

chooha·2025년 7월 21일

가디언앤시커

목록 보기
21/25

📝 개발일지 - 룬 배치 시스템 UX 개선 (대체 배치 시스템)

👨💻 오늘의 개발 작업

오늘은 유저 테스트에서 받은 피드백을 바탕으로 룬 시스템의 배치 메커닉을 근본적으로 개선했음

기존 방식은 빈 공간에만 배치가 가능했는데, 유저들이 "기존 룬과 겹치는 위치에 배치할 때 자동으로 교체되었으면 좋겠다"는 의견을 주셨음

새로운 방식은 3가지 배치 상태(빈공간/기존룬교체/배치불가)를 명확히 구분하고, 하나의 통합된 API로 모든 배치 케이스를 처리하는 직관적인 시스템을 구현했음


💡 오늘의 5분 기록

1. 유저 피드백 분석 및 문제점 파악

유저 테스트에서 룬 배치에 대한 불편함이 지속적으로 제기되었음

기존 방식의 한계:

  • 빈 공간에만 배치 가능 (기존 룬과 겹치면 배치 불가)
  • UI에서 복잡한 분기 처리 필요

핵심 문제

// 복잡한 분기 처리
if (BoardManager->CanPlaceRuneAt(SelectedRuneID, PlacementPos))
{
    // 빈 공간 배치
    bPlaceSuccess = BoardManager->PlaceRune(SelectedRuneID, PlacementPos);
}
else
{
    // 배치 실패
}

2. 새로운 접근 방식 설계 - 3상태 시스템

핵심 아이디어: "배치 가능성을 3가지 상태로 명확히 구분"

// 새로운 enum 추가
enum class EPlacementResult : uint8
{
    Valid,           // 빈 공간 (녹색 미리보기)
    ReplaceExisting, // 기존 룬 있음 (주황색 미리보기)  
    OutOfBounds      // 격자 범위 밖 (빨간색 미리보기)
};

API 통합 설계

// 기존: 여러 함수 분산
bool CanPlaceRuneAt()
bool PlaceRune()
bool PreviewRunePlacement()

// 새로운: 통합된 2개 함수
EPlacementResult CheckRunePlacement()  // 상태 확인
bool PlaceRune()                       // 통합 배치

3. CheckRunePlacement 함수 구현

핵심 로직 구현

EPlacementResult UGS_ArcaneBoardManager::CheckRunePlacement(uint8 RuneID, const FIntPoint& Pos, TArray<uint8>& OutAffectedRuneIDs)
{
    TArray<FIntPoint> RuneShape;
    if (!GetRuneShape(RuneID, RuneShape)) return EPlacementResult::OutOfBounds;
    
    bool bHasOverlapping = false;
    bool bOutOfBounds = false;
    TSet<uint8> OverlappingRuneIDSet;
    
    for (const FIntPoint& Offset : RuneShape)
    {
        FIntPoint CellPos = Pos + Offset;
        
        // 격자 범위 체크
        if (!CurrGridState.Contains(CellPos))
        {
            bOutOfBounds = true;
            continue;
        }
        
        // 기존 룬 겹침 체크
        const FGridCellData& CellData = CurrGridState[CellPos];
        if (CellData.State == EGridCellState::Occupied && CellData.PlacedRuneID > 0)
        {
            bHasOverlapping = true;
            OverlappingRuneIDSet.Add(CellData.PlacedRuneID);
        }
    }
    
    OutAffectedRuneIDs = OverlappingRuneIDSet.Array();
    
    if (bOutOfBounds) return EPlacementResult::OutOfBounds;
    else if (bHasOverlapping) return EPlacementResult::ReplaceExisting;
    else return EPlacementResult::Valid;
}

4. PlaceRune 함수 통합

단일 함수로 모든 배치 케이스 처리

bool UGS_ArcaneBoardManager::PlaceRune(uint8 RuneID, const FIntPoint& Pos, TArray<uint8>& OutRemovedRunes)
{
    OutRemovedRunes.Empty();
    
    // 배치 가능성 체크
    TArray<uint8> AffectedRuneIDs;
    EPlacementResult PlacementResult = CheckRunePlacement(RuneID, Pos, AffectedRuneIDs);
    
    // 격자 범위 밖이면 실패
    if (PlacementResult == EPlacementResult::OutOfBounds) return false;
    
    // 겹치는 룬들이 있으면 제거
    if (PlacementResult == EPlacementResult::ReplaceExisting)
    {
        for (uint8 OverlappingRuneID : AffectedRuneIDs)
        {
            if (RemoveRune(OverlappingRuneID))
            {
                OutRemovedRunes.Add(OverlappingRuneID);
            }
        }
    }
    
    // 룬 배치 (기존 로직과 동일)
    // ... 배치 로직
    
    return true;
}

5. 미리보기 시스템 개선

3색상 미리보기 시스템 구현

// UI 위젯의 색상 상태 추가
enum class EGridCellVisualState : uint8
{
    Normal,          // 기본 상태
    Valid,           // 녹색 (빈 공간)
    ReplaceExisting, // 주황색 (기존 룬 교체)
    Invalid          // 빨간색 (배치 불가)
};

색상 적용 로직

void UGS_RuneGridCellWidget::SetPreviewVisualState(EGridCellVisualState NewState)
{
    switch (NewState)
    {
    case EGridCellVisualState::Valid:
        PreviewImage->SetColorAndOpacity(FLinearColor(0.f, 1.f, 0.f, 0.2f)); // 녹색
        break;
    case EGridCellVisualState::ReplaceExisting:
        PreviewImage->SetColorAndOpacity(FLinearColor(1.f, 0.65f, 0.f, 0.2f)); // 주황색
        break;
    case EGridCellVisualState::Invalid:
        PreviewImage->SetColorAndOpacity(FLinearColor(1.f, 0.f, 0.f, 0.2f)); // 빨간색
        break;
    }
}

6. UI 로직 대폭 간소화

배치 로직 단순화

// 기존: 복잡한 분기 처리 (30+ 줄)
if (BoardManager->CanPlaceRuneAt(SelectedRuneID, PlacementPos))
{
    // 빈 공간 배치 로직...
}
else
{
    // 기존 룬 교체 로직...
}

// 새로운: 통합된 단순 처리 (10줄)
TArray<uint8> RemovedRunes;
bool bPlaceSuccess = BoardManager->PlaceRune(SelectedRuneID, PlacementPos, RemovedRunes);

if (bPlaceSuccess)
{
    // 성공 처리...
    for (uint8 RemovedRuneID : RemovedRunes)
    {
        RuneInven->UpdatePlacedStateOfRune(RemovedRuneID, false);
    }
}

7. 트러블슈팅 - 빨간색 미리보기 문제 ⚠️

문제 발견
격자 범위 밖으로 룬을 드래그할 때 빨간색 미리보기가 표시되지 않는 문제 발생

원인 분석

// 문제: 격자 범위 밖의 셀들은 GridCells에 존재하지 않음
for (const FIntPoint& Offset : RuneShape)
{
    FIntPoint CellPos = ReferenceCellPos + Offset;
    
    if (GridCells.Contains(CellPos))  // 범위 밖 셀은 여기서 걸러짐
    {
        // 색상 적용 불가능
    }
}

해결 방법

// 격자 범위 내에 있는 룬의 일부분이라도 빨간색으로 표시
// 사용자에게 "이 룬은 배치할 수 없다"는 것을 명확히 전달
void UGS_ArcaneBoardWidget::UpdateGridPreview(uint8 RuneID, const FIntPoint& ReferenceCellPos) 
{
    EPlacementResult PlacementResult = BoardManager->CheckRunePlacement(RuneID, ReferenceCellPos, AffectedRuneIDs);
    
    // 결과에 따른 색상 결정
    EGridCellVisualState PreviewState;
    switch (PlacementResult)
    {
    case EPlacementResult::OutOfBounds:
        PreviewState = EGridCellVisualState::Invalid; // 전체 빨간색
        break;
    }
    
    // 격자 범위 내 셀들만 색상 적용
    for (const FIntPoint& Offset : RuneShape)
    {
        FIntPoint CellPos = ReferenceCellPos + Offset;
        if (GridCells.Contains(CellPos))  // 범위 내 셀만
        {
            CellWidget->SetPreviewVisualState(PreviewState);
        }
    }
}

8. 성과와 개선 효과

UX 개선 결과

  • ✅ 직관적 배치: 기존 룬 위에 배치 시도하면 자동으로 교체됨
  • ✅ 명확한 시각 피드백: 3색상으로 배치 상태를 즉시 파악 가능
  • ✅ API 단순화: 여러 함수를 하나로 통합하여 유지보수성 향상
  • ✅ 코드 중복 제거: 중복된 로직을 하나의 함수로 통합

개발자 경험 개선

// 기존: 복잡한 API 사용
bool canPlace = BoardManager->CanPlaceRuneAt(id, pos);
TArray<FIntPoint> cells;
bool preview = BoardManager->PreviewRunePlacement(id, pos, cells);

if (canPlace) 
{
    bool place = BoardManager->PlaceRune(id, pos);
}

// 새로운: 단순한 API 사용
TArray<uint8> affected;
EPlacementResult result = BoardManager->CheckRunePlacement(id, pos, affected);
bool place = BoardManager->PlaceRune(id, pos, removed);

0개의 댓글