Unreal 개발 본 캠프 65일차

HappyCircle·2026년 3월 6일

Unreal 개발

목록 보기
82/163

📘 TIL —피로도, Hybrid GAS 기반 재사용 가능한 Monster Framework 설계

코딩테스트 - 피로도


1. 문제 이해: 던전 탐험 조건

현재 피로도 k가 주어지고 각 던전마다 다음 두 가지 값이 존재한다.

  • 최소 필요 피로도 req
  • 탐험 후 소모 피로도 cost

던전은 하루에 한 번만 방문 가능하며
최대 몇 개의 던전을 탐험할 수 있는지 구하는 문제이다.

문제에서 가장 중요한 조건은 던전 개수의 제한이다.

n ≤ 8

이 조건 때문에 가능한 모든 방문 조합의 수는

2^n

최대

2^8 = 256

이다.

즉 이 문제는 완전 탐색이 가능한 문제이다.


2. 처음 접근에서 막혔던 이유

문제를 처음 봤을 때 가장 막혔던 부분은 어떤 알고리즘을 떠올려야 하는지였다.

직관적으로는 다음과 같은 생각이 들었다.

  • 던전 순서에 따라 결과가 달라진다.
  • 가능한 모든 경우를 확인해야 할 것 같다.
  • 하지만 이를 어떤 방식으로 구현해야 할지 바로 떠오르지 않았다.

탐색 범위를 먼저 판단하지 못했던 것이 문제였다.


3. 탐색 범위 분석

던전이 최대 8개이므로 가능한 방문 순서는

8! = 40320

또는 방문 조합 기준으로

2^8 = 256

이다.

이 수치는 코딩 테스트에서 충분히 완전 탐색이 가능한 범위이다.

따라서 이 문제는

  • DFS를 이용한 순열 탐색
  • 비트마스크를 이용한 상태 탐색

같은 완전 탐색 기반 접근을 사용할 수 있다.

이번 풀이에서는 비트마스크 + DP 방식을 사용하였다.


4. 비트마스크를 사용하는 이유

던전 방문 여부를 배열로 관리할 수도 있지만
비트마스크를 사용하면 하나의 정수로 방문 상태를 표현할 수 있다.

예를 들어 던전이 3개일 때

mask = 101

이 의미하는 것은

2번 던전 방문
1번 던전 미방문
0번 던전 방문

던전 하나를 하나의 비트로 표현할 수 있다.

이 방식의 장점은 다음과 같다.

  • 방문 상태를 하나의 숫자로 표현 가능
  • 방문 여부를 빠르게 확인 가능
  • 새로운 상태를 쉽게 생성 가능

5. 비트마스크 상태 개수

던전이 n개라면 가능한 방문 상태는

2^n

비트 연산으로 표현하면

1 << n

예시

1 << 3
0001 → 1000

십진수 값

8

1 << n = 2^n

따라서 모든 상태를 탐색할 때는

for(mask = 0; mask < (1<<n); mask++)

형태로 순회한다.


6. 비트 연산을 사용하는 방법

비트마스크 문제에서는 세 가지 연산을 가장 많이 사용한다.

방문 여부 확인

mask & (1<<i)

i번째 비트가 1인지 확인

방문 추가

mask | (1<<i)

i번째 비트를 1로 설정

방문 제거

mask & ~(1<<i)

i번째 비트를 0으로 변경

이 연산들을 이용해 던전 방문 상태를 관리할 수 있다.


7. 비트 번호와 던전 번호 관계

비트 번호는 오른쪽부터 0번 비트이다.

예시

mask = 101

비트 구조

bit2 bit1 bit0
 1    0    1

던전 방문 상태

2번 던전 방문
1번 던전 미방문
0번 던전 방문

따라서

1 << i

i번 던전을 의미하게 된다.


8. 방문한 던전 개수 계산

방문한 던전 개수는 다음 함수를 이용해 계산할 수 있다.

__builtin_popcount(mask)

이 함수는 이진수에서 1의 개수를 반환한다.

예시

mask = 1011

결과

3

방문한 던전의 개수가 된다.


9. 비트마스크 DP의 핵심 아이디어

DP 상태는 다음과 같이 정의한다.

dp[mask] = 해당 방문 상태로 도달했을 때 남아있는 최대 피로도

초기 상태

dp[0] = k

각 상태에서 아직 방문하지 않은 던전을 탐색하고
조건을 만족하면 새로운 상태를 만든다.

nextMask = mask | (1<<i)

남은 피로도

nextFatigue = dp[mask] - cost[i]

그리고

dp[nextMask] = max(dp[nextMask], nextFatigue)

로 상태를 갱신한다.


10. 이번 문제에서 얻은 핵심 교훈

1️⃣ 입력 크기를 먼저 확인하기

n ≤ 8

이면 대부분

  • 완전 탐색
  • DFS
  • 비트마스크

중 하나로 해결 가능하다.


2️⃣ 상태 조합 개수를 먼저 계산하기

2^n

이 충분히 작다면 모든 상태 탐색을 고려할 수 있다.


3️⃣ 비트마스크는 부분집합 탐색에서 매우 강력한 도구

특히 다음 유형에서 자주 등장한다.

  • 방문 여부 관리
  • 부분집합 탐색
  • 비트마스크 DP

11. 문제 풀이 전 스스로 던질 질문

문제를 풀기 전에 다음 질문을 해보는 것이 중요하다.

1️⃣ 입력 크기가 완전 탐색 가능한 범위인가?

2️⃣ 상태를 비트로 표현할 수 있는가?

3️⃣ 방문 조합이 2^n 형태인가?

이 질문을 먼저 확인하면 풀이 접근 방향을 훨씬 빠르게 잡을 수 있다.

Hybrid GAS 기반 재사용 가능한 Monster Framework 설계

오늘은 팀프로젝트에서 몬스터 구현한 부분에서 수정했으면 하는 부분들을 반영하여 재사용 가능한 몬스터 프레임워크 구조 설계를 진행했다.
목표는 단순히 현재 프로젝트에서 동작하는 몬스터 구조가 아니라 다른 프로젝트에서도 쉽게 이식 가능한 구조를 만드는 것이었다.

특히 다음 4가지 목표를 중심으로 설계를 정리했다.

  • 몬스터 하나에 여러 특수 기믹을 자유롭게 조합 가능
  • 랜덤으로 특수 기믹을 할당 가능
  • 다른 프로젝트에도 쉽게 가져다 붙일 수 있는 구조
  • 프로젝트 전체가 GAS가 아니어도 몬스터만 독립적으로 동작 가능

이를 위해 Hybrid GAS 기반 Monster Framework 구조로 설계를 정리했다.


기존 몬스터 구조의 문제점

기존 PotatoMonster 구조에서는 몬스터 본체가 너무 많은 책임을 가지고 있었다.

예를 들어 다음과 같은 기능들이 몬스터 클래스나 컴포넌트에 직접 묶여 있었다.

  • 전투 처리
  • 특수 스킬 실행
  • 버프 / DOT 관리
  • 타겟 판별
  • 플레이어 / 구조물 판별
  • UI 연동
  • 웨이브 시스템 연결

이 구조에서는 다음 문제가 발생한다.

  • 특수 기믹이 늘어날수록 클래스가 계속 비대해짐
  • 여러 기믹을 동시에 붙이면 충돌 가능성 증가
  • 프로젝트 의존성이 강해서 다른 프로젝트로 이식이 어려움

그래서 몬스터 본체를 최대한 얇게 만들고 기능을 분리하는 방향으로 구조를 재설계했다.


최종 설계 원칙

새 구조에서는 다음 원칙을 기준으로 설계를 진행했다.

  • 몬스터 본체는 최대한 껍데기 역할
  • 실제 기능은 Trait / Ability 단위
  • 수치 변화는 Gameplay Effect
  • 외부 시스템 연결은 Interface + Spec + Bridge
  • 프로젝트 의존성은 Integration 레이어로 분리

이 구조를 통해 확장성과 재사용성을 동시에 확보하는 것을 목표로 했다.


전체 아키텍처 구조

최종 구조는 크게 3층 아키텍처로 구성된다.

MonsterFramework

범용 몬스터 코어 프레임워크

주요 역할

  • 몬스터 본체
  • AbilitySystem 기반 능력 시스템
  • Trait 조합 시스템
  • 타겟팅
  • AI 공통 로직
  • 외부 시스템 브리지
  • 스폰 규격

이 레이어는 다른 프로젝트에서도 그대로 사용할 수 있어야 한다.


PotatoMonsterIntegration

현재 Potato 프로젝트 전용 연결 레이어

주요 역할

  • Potato 플레이어 연결
  • Potato 구조물 연결
  • LanePath 시스템 연결
  • Potato UI / 보스바
  • GameMode / DayNight / Reward 시스템 연결

MonsterFramework와 Potato 프로젝트 사이의 어댑터 계층이다.


Project Content

프로젝트별 데이터 / 밸런스 / 에셋 레이어

포함 내용

  • 몬스터 DataAsset
  • Rank 정의
  • Trait 정의
  • 애니메이션 세트
  • VFX / SFX
  • 웨이브 데이터

몬스터 구조 핵심 개념

최종적으로 몬스터는 다음 구조로 정의된다.

Monster = Base + Rank + Traits

Base

몬스터의 기본 몸체

예시

  • Skeleton
  • Cactus
  • Dragon

포함 내용

  • 외형
  • 기본 이동
  • 기본 공격
  • 기본 Attribute
  • 기본 Ability

Rank

몬스터 강화 계층

예시

  • Normal
  • Elite
  • Boss

포함 내용

  • HP 배율
  • 공격력 배율
  • 이동속도 배율
  • 추가 Trait
  • 추가 태그
  • UI / 연출 차이

Traits

특수 기믹 단위

예시

  • Split
  • HardenShell
  • FirePillar
  • AuraBurn
  • DeathExplode
  • OnHitThorns

Trait 여러 개를 조합하여 한 몬스터가 여러 특수 기믹을 동시에 가지는 구조를 만든다.


Ability / Effect / 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


랜덤 Trait 조합 시스템

랜덤 기믹 부여는 MonsterTraitResolver가 담당한다.

Resolver가 사용하는 요소

  • Trait Pool
  • Trait Cost
  • Trait Weight
  • Required Tags
  • Blocked Tags
  • Rank Trait Budget

예시

Elite 몬스터 예산 = 4

Trait 비용

Split = 2
HardenShell = 1
FirePillar = 3
AuraBurn = 2

가능한 조합

Split + HardenShell
FirePillar + HardenShell
AuraBurn + HardenShell
Split + AuraBurn

이 구조를 통해 다양한 몬스터 조합을 자동 생성할 수 있다.


Hybrid GAS 구조

이 설계의 핵심은 Hybrid GAS 구조다.

몬스터 내부

  • GAS 사용

외부 시스템

  • GAS 사용하지 않아도 됨

  • 플레이어
  • 구조물
  • 기존 데미지 시스템

이 모두가 비GAS여도 몬스터는 GAS 기반 시스템을 사용할 수 있다.


외부 시스템 연결 구조

외부 시스템과는 직접 클래스 참조를 하지 않는다.

대신 다음 구조로 연결한다.

Interface
→ 공통 규약

Spec
→ 데이터 구조

Bridge
→ 시스템 번역


Interfaces

  • ICombatTargetInterface
  • IExternalDamageableInterface
  • IExternalDamageReceiverInterface
  • IExternalStatusInterface
  • ITeamAgentInterface
  • IMonsterPathProviderInterface

Spec

FExternalDamageSpec
FExternalStatusSpec
FMonsterAttackRequest

외부 ↔ 몬스터 간 데이터 전달 규격


Bridge

UMonsterDamageBridgeSubsystem
UMonsterStatusBridgeSubsystem

GAS ↔ 비GAS 시스템 변환 담당


몬스터 공격 처리 규칙

몬스터가 외부 대상을 공격할 때

  1. Target이 ASC 존재 → GAS 경로
  2. 없으면 ExternalDamageableInterface
  3. 그것도 없으면 ApplyDamage fallback

이 방식으로 GAS / 비GAS 모두 지원한다.


MonsterFramework 설계에서 지켜야 할 규칙

MonsterFramework 코어에는 프로젝트 의존성을 절대 넣지 않는다.

예를 들어 다음 것들은 금지 대상이다.

  • PotatoPlayerCharacter
  • PotatoPlaceableStructure
  • PotatoGameMode
  • PotatoPlayerHUD
  • Potato 전용 Niagara / SFX
  • Potato DataTable row name

이들은 모두 Integration 레이어로 분리한다.


Trait 기반 구조 도입

기존에는 다음과 같은 방식이었다.

  • AuraComponent
  • SplitComponent
  • HardenShellComponent

이들을 MonsterBase에 직접 붙이는 구조였다.

하지만 이 방식은 확장성이 떨어진다.

그래서 다음 구조로 변경했다.

UMFMonsterTraitComponent
→ Trait 공통 베이스

UMFHardenShellTraitComponent
UMFSplitTraitComponent
UMFAuraTraitComponent

MonsterBase는 Trait을 직접 알지 않고

  • Trait 배열만 보유
  • Trait 이벤트 전달

역할만 수행한다.


MonsterBase 역할 재정의

MonsterBase는 이제 다음 역할만 담당한다.

  • 전투 본체
  • Trait 이벤트 허브
  • 외부 데미지 수신 입구

Trait에게 전달하는 이벤트

  • OnMonsterSpawned
  • OnMonsterDamaged
  • OnMonsterDeath
  • OnReceiveExternalDamage
  • ModifyIncomingDamage

오늘 작업한 실제 구현 진행 상태

현재 MonsterFramework 플러그인에 다음 구조를 생성했다.

  • Core
  • Components
  • AI
  • Data
  • Spawning
  • AbilitySystem
  • Types
  • Interfaces
  • Bridge
  • Traits

또한 다음 주요 클래스 골격을 생성했다.

  • MFMonsterBase
  • MFCombatComponent
  • MFTargetingComponent
  • MFMovementComponent
  • MFSpecialSkillComponent
  • MFMonsterAIController
  • MFMonsterTraitComponent
  • MFMonsterTraitResolver
  • MFMonsterDamageBridgeSubsystem
  • MFExternalCombatTypes

현재 단계는 프레임워크 골격을 완성하는 단계다.


오늘 설계의 핵심 한 줄 정리

Hybrid GAS 기반 Monster Framework를 설계하여
Trait + Ability + Effect 조합 구조로 몬스터 기능을 분리하고
Interface + Bridge를 통해 비GAS 프로젝트에서도 동작 가능한 재사용 구조를 만들었다.

profile
개발합시다!

0개의 댓글