현재 피로도 k가 주어지고 각 던전마다 다음 두 가지 값이 존재한다.
reqcost던전은 하루에 한 번만 방문 가능하며
최대 몇 개의 던전을 탐험할 수 있는지 구하는 문제이다.
문제에서 가장 중요한 조건은 던전 개수의 제한이다.
n ≤ 8
이 조건 때문에 가능한 모든 방문 조합의 수는
2^n
최대
2^8 = 256
이다.
즉 이 문제는 완전 탐색이 가능한 문제이다.
문제를 처음 봤을 때 가장 막혔던 부분은 어떤 알고리즘을 떠올려야 하는지였다.
직관적으로는 다음과 같은 생각이 들었다.
즉 탐색 범위를 먼저 판단하지 못했던 것이 문제였다.
던전이 최대 8개이므로 가능한 방문 순서는
8! = 40320
또는 방문 조합 기준으로
2^8 = 256
이다.
이 수치는 코딩 테스트에서 충분히 완전 탐색이 가능한 범위이다.
따라서 이 문제는
같은 완전 탐색 기반 접근을 사용할 수 있다.
이번 풀이에서는 비트마스크 + DP 방식을 사용하였다.
던전 방문 여부를 배열로 관리할 수도 있지만
비트마스크를 사용하면 하나의 정수로 방문 상태를 표현할 수 있다.
예를 들어 던전이 3개일 때
mask = 101
이 의미하는 것은
2번 던전 방문
1번 던전 미방문
0번 던전 방문
즉 던전 하나를 하나의 비트로 표현할 수 있다.
이 방식의 장점은 다음과 같다.
던전이 n개라면 가능한 방문 상태는
2^n
비트 연산으로 표현하면
1 << n
예시
1 << 3
0001 → 1000
십진수 값
8
즉
1 << n = 2^n
따라서 모든 상태를 탐색할 때는
for(mask = 0; mask < (1<<n); mask++)
형태로 순회한다.
비트마스크 문제에서는 세 가지 연산을 가장 많이 사용한다.
mask & (1<<i)
i번째 비트가 1인지 확인
mask | (1<<i)
i번째 비트를 1로 설정
mask & ~(1<<i)
i번째 비트를 0으로 변경
이 연산들을 이용해 던전 방문 상태를 관리할 수 있다.
비트 번호는 오른쪽부터 0번 비트이다.
예시
mask = 101
비트 구조
bit2 bit1 bit0
1 0 1
던전 방문 상태
2번 던전 방문
1번 던전 미방문
0번 던전 방문
따라서
1 << i
는 i번 던전을 의미하게 된다.
방문한 던전 개수는 다음 함수를 이용해 계산할 수 있다.
__builtin_popcount(mask)
이 함수는 이진수에서 1의 개수를 반환한다.
예시
mask = 1011
결과
3
즉 방문한 던전의 개수가 된다.
DP 상태는 다음과 같이 정의한다.
dp[mask] = 해당 방문 상태로 도달했을 때 남아있는 최대 피로도
초기 상태
dp[0] = k
각 상태에서 아직 방문하지 않은 던전을 탐색하고
조건을 만족하면 새로운 상태를 만든다.
nextMask = mask | (1<<i)
남은 피로도
nextFatigue = dp[mask] - cost[i]
그리고
dp[nextMask] = max(dp[nextMask], nextFatigue)
로 상태를 갱신한다.
n ≤ 8
이면 대부분
중 하나로 해결 가능하다.
2^n
이 충분히 작다면 모든 상태 탐색을 고려할 수 있다.
특히 다음 유형에서 자주 등장한다.
문제를 풀기 전에 다음 질문을 해보는 것이 중요하다.
1️⃣ 입력 크기가 완전 탐색 가능한 범위인가?
2️⃣ 상태를 비트로 표현할 수 있는가?
3️⃣ 방문 조합이 2^n 형태인가?
이 질문을 먼저 확인하면 풀이 접근 방향을 훨씬 빠르게 잡을 수 있다.
오늘은 팀프로젝트에서 몬스터 구현한 부분에서 수정했으면 하는 부분들을 반영하여 재사용 가능한 몬스터 프레임워크 구조 설계를 진행했다.
목표는 단순히 현재 프로젝트에서 동작하는 몬스터 구조가 아니라 다른 프로젝트에서도 쉽게 이식 가능한 구조를 만드는 것이었다.
특히 다음 4가지 목표를 중심으로 설계를 정리했다.
이를 위해 Hybrid GAS 기반 Monster Framework 구조로 설계를 정리했다.
기존 PotatoMonster 구조에서는 몬스터 본체가 너무 많은 책임을 가지고 있었다.
예를 들어 다음과 같은 기능들이 몬스터 클래스나 컴포넌트에 직접 묶여 있었다.
이 구조에서는 다음 문제가 발생한다.
그래서 몬스터 본체를 최대한 얇게 만들고 기능을 분리하는 방향으로 구조를 재설계했다.
새 구조에서는 다음 원칙을 기준으로 설계를 진행했다.
이 구조를 통해 확장성과 재사용성을 동시에 확보하는 것을 목표로 했다.
최종 구조는 크게 3층 아키텍처로 구성된다.
범용 몬스터 코어 프레임워크
주요 역할
이 레이어는 다른 프로젝트에서도 그대로 사용할 수 있어야 한다.
현재 Potato 프로젝트 전용 연결 레이어
주요 역할
즉 MonsterFramework와 Potato 프로젝트 사이의 어댑터 계층이다.
프로젝트별 데이터 / 밸런스 / 에셋 레이어
포함 내용
최종적으로 몬스터는 다음 구조로 정의된다.
Monster = Base + Rank + Traits
몬스터의 기본 몸체
예시
포함 내용
몬스터 강화 계층
예시
포함 내용
특수 기믹 단위
예시
Trait 여러 개를 조합하여 한 몬스터가 여러 특수 기믹을 동시에 가지는 구조를 만든다.
특수 기믹을 구현할 때 다음 역할 분리를 사용한다.
Trait
→ 기믹 단위 정의
Ability
→ 실제 행동 실행
Effect
→ 수치 변화
예시
Split
Trait
Trait.Split
Ability
GA_Monster_SplitOnDeath
HardenShell
Trait
Trait.HardenShell
Ability
GA_Monster_HardenShell
Effect
GE_Monster_HardenShellBuff
FirePillar
Trait
Trait.FirePillar
Ability
GA_Monster_FirePillar
Effect
GE_Monster_DOT_Burning
랜덤 기믹 부여는 MonsterTraitResolver가 담당한다.
Resolver가 사용하는 요소
예시
Elite 몬스터 예산 = 4
Trait 비용
Split = 2
HardenShell = 1
FirePillar = 3
AuraBurn = 2
가능한 조합
Split + HardenShell
FirePillar + HardenShell
AuraBurn + HardenShell
Split + AuraBurn
이 구조를 통해 다양한 몬스터 조합을 자동 생성할 수 있다.
이 설계의 핵심은 Hybrid GAS 구조다.
몬스터 내부
외부 시스템
즉
이 모두가 비GAS여도 몬스터는 GAS 기반 시스템을 사용할 수 있다.
외부 시스템과는 직접 클래스 참조를 하지 않는다.
대신 다음 구조로 연결한다.
Interface
→ 공통 규약
Spec
→ 데이터 구조
Bridge
→ 시스템 번역
FExternalDamageSpec
FExternalStatusSpec
FMonsterAttackRequest
외부 ↔ 몬스터 간 데이터 전달 규격
UMonsterDamageBridgeSubsystem
UMonsterStatusBridgeSubsystem
GAS ↔ 비GAS 시스템 변환 담당
몬스터가 외부 대상을 공격할 때
이 방식으로 GAS / 비GAS 모두 지원한다.
MonsterFramework 코어에는 프로젝트 의존성을 절대 넣지 않는다.
예를 들어 다음 것들은 금지 대상이다.
이들은 모두 Integration 레이어로 분리한다.
기존에는 다음과 같은 방식이었다.
이들을 MonsterBase에 직접 붙이는 구조였다.
하지만 이 방식은 확장성이 떨어진다.
그래서 다음 구조로 변경했다.
UMFMonsterTraitComponent
→ Trait 공통 베이스
UMFHardenShellTraitComponent
UMFSplitTraitComponent
UMFAuraTraitComponent
MonsterBase는 Trait을 직접 알지 않고
역할만 수행한다.
MonsterBase는 이제 다음 역할만 담당한다.
Trait에게 전달하는 이벤트
현재 MonsterFramework 플러그인에 다음 구조를 생성했다.
또한 다음 주요 클래스 골격을 생성했다.
현재 단계는 프레임워크 골격을 완성하는 단계다.
Hybrid GAS 기반 Monster Framework를 설계하여
Trait + Ability + Effect 조합 구조로 몬스터 기능을 분리하고
Interface + Bridge를 통해 비GAS 프로젝트에서도 동작 가능한 재사용 구조를 만들었다.