[25.06.13] :: 가디언 앤 시커 프로젝트 13

chooha·2025년 6월 14일

가디언앤시커

목록 보기
13/25

📝 개발일지 - 아케인보드 룬 연결로직 시스템

👨‍💻 오늘의 개발 작업

오늘은 아케인보드 시스템에서 특수 셀을 시작점으로 연결된 룬들에게 보너스 스탯을 부여하는 연결로직 시스템을 구현했음

처음엔 단순히 룬 ID만 저장하면 될 줄 알았는데, UI 시각효과와 보너스 계산을 고려하니 데이터 구조 설계가 생각보다 복잡했음. 특히 어떤 자료구조를 사용할지, 어떤 탐색 알고리즘을 쓸지에 대한 고민이 많았음


💡 오늘의 5분 기록

1. 요구사항 분석 - 룬 연결 시스템의 핵심

특수 그리드 셀 하나에서 시작해서 인접한 룬들을 탐색하고, 연결된 모든 룬에 보너스 스탯을 부여하는 시스템이 필요했음

시스템 규모

  • 그리드 셀: 9개 (3x3)
  • 룬 최대 개수: 5개 정도
  • 하나의 룬이 여러 룬과 연결 가능
  • UI에서 연결된 룬들에 시각 효과 적용 필요

2. 자료구조 선택의 고민 - 최종 결정까지의 여정

1차 고민: 연결된 룬 ID만 저장

TArray<uint8> ConnectedRuneIDs;

문제점: UI에서 시각 효과를 위해 룬이 차지하는 셀 위치가 필요한데 역추적이 복잡함

2차 고민: 연결된 셀 위치만 저장

TArray<FIntPoint> ConnectedCellPositions;

문제점: 보너스 스탯 계산 시 어떤 룬인지 알기 어려움

최종 결정: 룬별 셀 위치 저장

// FArcaneBoardStats 구조체 내부
TMap<uint8, TArray<FIntPoint>> ConnectedRuneCells;

결정 이유:

  • UI 시각 효과: 룬별로 다른 효과 적용 가능
  • 보너스 계산: 룬 ID로 직접 접근
  • 디버깅: 어떤 룬이 어떤 셀을 차지하는지 명확
  • 확장성: 룬 타입별 다른 처리 가능

3. 탐색 알고리즘 결정 - DFS vs BFS

DFS 선택 이유:

void FindConnectedCells(const FIntPoint CellPos, TSet<FIntPoint>& VisitedCell) {
    // 방문 체크
    if (VisitedCell.Contains(CellPos)) return;
    
    // 빈 셀에서 가지 중단
    FGridCellData CellData;
    if (!GetCellData(CellPos, CellData) || CellData.State == EGridCellState::Empty) {
        return;
    }
    
    // 현재 셀 처리
    VisitedCell.Add(CellPos);
    uint8 RuneID = CellData.PlacedRuneID;
    if(!CurrBoardStats.ConnectedRuneCells.Contains(RuneID)) {
        CurrBoardStats.ConnectedRuneCells.Add(RuneID, TArray<FIntPoint>());
    }
    CurrBoardStats.ConnectedRuneCells[RuneID].Add(CellPos);
    
    // 4방향 재귀 탐색
    TArray<FIntPoint> Directions = { {0, 1}, {0, -1}, {1, 0}, {-1, 0} };
    for (const FIntPoint& Dir : Directions) {
        FIntPoint NextPos = CellPos + Dir;
        FindConnectedCells(NextPos, VisitedCell);
    }
}

핵심 장점:

  • 빈 셀 만나면 즉시 해당 방향 중단
  • 재귀로 간단하고 직관적인 구현
  • 룬이 5개 정도로 작아서 성능 차이 없음

4. 데이터 전달 방식 고민

1차: 기존 스탯 델리게이트 활용

OnStatsChanged.Broadcast(CurrStatEffects);  // FGS_StatRow만 전달

문제: UI 시각 효과를 위한 연결 정보가 없음

2차: 매개변수 두 개로 분리

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnConnectionsChangedDelegate,
    const FGS_StatRow&, RuneStats,
    const TMap<uint8, TArray<FIntPoint>>&, ConnectedRuneCells);

문제:
TMap<uint8, TArray>와 같은 타입은 매개변수로 사용 안됨
보너스 스탯을 UI에서 계산해야 하는 책임 분리 문제

최종: 통합 구조체로 해결

// FArcaneBoardStats 구조체
struct FArcaneBoardStats {
    FGS_StatRow RuneStats;      // 룬 기본 스탯
    FGS_StatRow BonusStats;     // 연결 보너스 스탯
    TMap<uint8, TArray<FIntPoint>> ConnectedRuneCells;  // 시각효과용
};

// 델리게이트는 단순하게
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStatsChangedDelegate, 
    const FArcaneBoardStats&, BoardStats);

실제 사용:

void CalculateStatEffects() {
    UpdateConnections();  // 연결 로직 먼저 실행
    
    // 기본 스탯 계산
    FGS_StatRow BaseResult, BonusResult;
    TMap<uint8, TArray<FIntPoint>> ConnectedRunes = CurrBoardStats.ConnectedRuneCells;
    float BonusValue = ConnectedRunes.Num();
    
    // 룬별 스탯 계산 및 연결 보너스 적용
    for (const FPlacedRuneInfo& Rune : PlacedRunes) {
        FRuneTableRow RuneData;
        if (GetRuneData(Rune.RuneID, RuneData)) {
            // 기본 스탯 추가
            if (RuneData.StatEffect.StatName == FName("HP")) {
                BaseResult.HP += RuneData.StatEffect.Value;
                
                // 연결 보너스 추가
                if (ConnectedRunes.Contains(Rune.RuneID) && BonusResult.HP == 0) {
                    BonusResult.HP = BonusValue;
                }
            }
            // 다른 스탯들도 동일하게...
        }
    }
    
    CurrBoardStats.RuneStats = BaseResult;
    CurrBoardStats.BonusStats = BonusResult;
}

5. 핵심 성과와 깨달음

구현 완료된 기능:

  • 특수 셀(SpecialCellPos) 기준 4방향 DFS 연결 탐색
  • 룬별 연결된 셀 위치 정보 저장 (TMap<uint8, TArray<FIntPoint>>)
  • UI는 받은 데이터만 표시하는 깔끔한 책임 분리

핵심 깨달음:

  • 작은 데이터셋(룬 5개)에서는 단순한 자료구조도 충분히 효율적
  • DFS의 가지 중단 특성이 룬 연결 로직에 완벽하게 맞음
  • 매니저-UI 간 데이터 전달은 통합 구조체가 확장성과 유지보수성 면에서 우수
  • 4방향 탐색으로 의도치 않은 대각선 연결 방지 가능

📚 개발 참고

데이터 구조 설계 원칙

  • 통합성: 관련된 데이터는 하나의 구조체로 묶기
  • 확장성: 나중에 새로운 정보 추가가 쉬운 구조
  • 책임 분리: 매니저는 계산, UI는 표시만

성능 최적화 포인트

  • 작은 데이터셋에서는 해시 테이블 오버헤드보다 단순 구조가 유리
  • 연결 로직은 룬 배치/제거 시에만 실행되므로 성능 부담 최소화

0개의 댓글