오늘은 유저 테스트에서 받은 피드백을 바탕으로 룬 시스템의 배치 메커닉을 근본적으로 개선했음
기존 방식은 빈 공간에만 배치가 가능했는데, 유저들이 "기존 룬과 겹치는 위치에 배치할 때 자동으로 교체되었으면 좋겠다"는 의견을 주셨음
새로운 방식은 3가지 배치 상태(빈공간/기존룬교체/배치불가)를 명확히 구분하고, 하나의 통합된 API로 모든 배치 케이스를 처리하는 직관적인 시스템을 구현했음
유저 테스트에서 룬 배치에 대한 불편함이 지속적으로 제기되었음
기존 방식의 한계:
핵심 문제
// 복잡한 분기 처리
if (BoardManager->CanPlaceRuneAt(SelectedRuneID, PlacementPos))
{
// 빈 공간 배치
bPlaceSuccess = BoardManager->PlaceRune(SelectedRuneID, PlacementPos);
}
else
{
// 배치 실패
}
핵심 아이디어: "배치 가능성을 3가지 상태로 명확히 구분"
// 새로운 enum 추가
enum class EPlacementResult : uint8
{
Valid, // 빈 공간 (녹색 미리보기)
ReplaceExisting, // 기존 룬 있음 (주황색 미리보기)
OutOfBounds // 격자 범위 밖 (빨간색 미리보기)
};
API 통합 설계
// 기존: 여러 함수 분산
bool CanPlaceRuneAt()
bool PlaceRune()
bool PreviewRunePlacement()
// 새로운: 통합된 2개 함수
EPlacementResult CheckRunePlacement() // 상태 확인
bool PlaceRune() // 통합 배치
핵심 로직 구현
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;
}
단일 함수로 모든 배치 케이스 처리
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;
}
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;
}
}
배치 로직 단순화
// 기존: 복잡한 분기 처리 (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);
}
}
문제 발견
격자 범위 밖으로 룬을 드래그할 때 빨간색 미리보기가 표시되지 않는 문제 발생
원인 분석
// 문제: 격자 범위 밖의 셀들은 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);
}
}
}
UX 개선 결과
개발자 경험 개선
// 기존: 복잡한 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);