JS DEVLOG : Github 자세히보기

언리얼 엔진 5

언리얼엔진5 C++로 정복하기(21) 업적 시스템 만들기: 데이터 기반 설계부터 UI 시퀀싱까지

Meshami 2026. 4. 9. 09:16

안녕하세요 오랜만에 정복하기 글을 써봅니다. 이제 최종 빌드를 하여 시간적 여유가 많이 생겨..이용등급 및 빌드 심사중이며 남는 시간에 차근차근 이번 Toosin: 투신 프로젝트를 개발하면서 블로그에 안쓴것들을 한번 써보고자 합니다.

이번 글은 조금 긴데, 언리얼엔진의 데이터 테이블 활용법과 서브시스템 아키텍처, 로직과 UI를 분리하는 델리게이트 그리고 업적이 동시에 달성될 때 팝업을 뛰어주는 큐 시스템 까지 주제를 담았습니다.

 

1. 업적 시스템 왜 데이터 기반 설계 일까?

언리얼 개발을 할때 초보자와 숙련자의 차이는 데이터와 로직을 얼마나 잘 분리하는가 라는 차이가 있다 생각합니다.

 

하드코딩 관련하여..

만약 캐릭터 클래스에서 업적에 관련한 모든 로직을 체크한다면..?

 

저러한 코드들이 엄청나게 길어지면서 나중에 버그 고치기도 힘들고 수치하나 바꾸고 컴파일 이러한 것들이 반복이 됩니다.

 

데이터 기반 설계를 하게된다면 로직만 C++로 짜두고 무엇을 달성하는지 (수치,이름,아이콘)는 데이터 테이블에 위임을 하게되면 이렇게 되면 차후 개발자나 디자이너 , 기획자가 에디터에서 숫자만 바꿔도 게임에 반영되고, C++코드는 단 한줄도 수정할 필요가 없습니다.

데이터 테이블 일부 사진

모든 업적들은 데이터 테이블에 관리되어, 언리얼 엔진에서 데이터 테이블의 한 행을 정의하려면, FTableRowBase 를 상속받은 구조체가 필요합니다.

왜 ConditionType이 중요하냐? 모든 업적을 매 프레임 체크하는 것은 자원 낭비입니다. 대신 몬스터가 죽으면 TOTALKILLS 타입의 업적들만 체크하도록 요청을 하면 됩니다. 필터링의 기초인데,

 

위의 업적 시스템을 관리하기 위해 GameInstanceSubsystem을 사용합니다.

https://dev.epicgames.com/documentation/unreal-engine/programming-subsystems-in-unreal-engine

 

Programming Subsystems in Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

An overview of programming subsystems in Unreal Engine.

dev.epicgames.com

언리얼 공식 문서(서브시스템)

 

싱글턴(Singleton) 패턴이란

프로그램 전체에서 특정 클래스의 인스턴스가 단 하나만 존재하도록 보장하는 디자인 패턴으로 어디서든 동일한 인스턴스에 접글할 수 있어 전역 관리자를 만들 때 주로 쓰인다.

고전적인 싱글턴 패턴으로 유니티에서 사용할때와 굉장히 비슷한 느낌입니다.

언리얼에서는 싱글턴이 좋지는 않습니다..!

이유 1: 게임이 꺼질 때 메모리 해제 타이밍을 잡기 어렵고, 에디터 프리뷰 등에서 인스턴스가 꼬일 수 있습니다.

이유 2: 언리얼의 가비지 컬렉션(GC) 시스템과 잘 맞지 않아 메모리 누수나 크래시의 원인이 되기도 합니다.

 

그 대안으로 등장한 것이 위의 공식문서에 나온 서브시스템입니다. 서브시스템은 싱글턴의 장점(전역 접근)은 그대로 가져오면서, 언리얼 엔진이 직접 인스턴스의 생성과 소멸을 관리해 줍니다.

1. GameInstance가 켜질 때 자동 생성, 꺼질 때 자동 소멸

2. 언리얼 가비지 컬렉터의 관리를 받아 안전

3. 예) 업적 관련 로직만 모아두어 코드 깔끔!


코드 설명

GetAllRows: 데이터 테이블의 모든 데이터를 배열로 가져오는 언리얼 내장 함수

TSet<FName> UnlockedAchievemets: 중복을 허용하지 않는 집합 자료구조로 일반 배열(TArray)은 특정 항목을 찾으려면 처음부터 끝까지 뒤져야 하지만, TSet은 해시 방식을 사용하기에 데이터가 아무리 많아도 시간복잡도 O(1)이라 즉시 찾아낼 수 있어 업적 달성 여부 확인에 최적입니다.

TMap<FName, int32> AchievementProgress: 열쇠(key)와 값(value)의 쌍으로 이루어진 자료구조로 업적 ID를 열쇠로 삼아 현재 진행도 수치를 짝지어 저장합니다. FindOrAdd 함수를 사용하면 코드를 아주 간결하게 유지할 수 있습니다.

 

멀티캐스트 델리게이트

업적을 해금하는 C++ 로직과 이를 화면에 뛰어주는 UI 사이클은 직접 연결하면 안됩니다. 만약 UI가 없는데 해금 로직이 UI를 호출하면 크래쉬가 나게되는데, 이때 필요한 것이 델리게이트(Delegate)입니다.

 

델리게이트의 역할: 

1. Broadcast: C++ 서브시스템은 업적이 해금되면 업적 터졌습니다. 이름은 이거고 아이콘은 이거라고 공중에 전파합니다.

2. Bind: UI위젯은 생성될 때 이 소리를 듣겠다고 귀를 기울입니다.

3. Decoupling: C++ 로직은 UI가 있는지 없는지 몰라도 됩니다. 일단 방송으로 전파하니깐요

 

UI 시퀀싱: 큐(Queue) 시스템과 순차적 노출

게임 플레이 중 업적 5개가 동시에 터졌다고 가정을 해봅시다. 팝업 5개가 동시에 화면 중앙에 겹쳐서 나오면 아무것도 안보이는 상황이 있습니다.(구현 초반에 제가 그랬습니다 ㅠ) 이를 해결 하기위해 큐 시스템을 사용했습니다.

일단 기본적으로 제가 현재 진행하고 있는 프로젝트에서 허브라는 맵이 메인화면을 담당하는데, 그곳에서는 UTSHubWidget이 모든 ui상호작용을 담당하는 위젯블루프린트 입니다. 스테이지가 종료하게 되면 다음 순서를 이벤트를 진행합니다.

1. 승리/패배 메시지 알림(결과)

2. 랜덤 이벤트 (발생했을 시에)

3. 대기 중인 업적 달성 알림

4. 다음 스테이지 시작시에 카운트 다운 시작

전공 공부하다 보면 알게되는 FIFO(First in First Out)원칙으로, 먼저 터진 업적을 보여주고, 그 팝업이 닫힐 때 OnPopupFinished를 다시 TryTriggerHunbEvents를 호출해 다음 업적을 꺼내오는 릴레이 방식입니다.

 

실제 활용 방법

데이터 테이블의 Threshhold를 10으로 설정하면, 서브시스템 내에서 if (CurrentValue <= Data->Threshhold) 로직을 통해 체력이 10퍼 이하로 남았을 때 승리라는 다소 복잡한 조건을 쉽게 처리할 수 있습니다.

 

이번에는 최종 빌드하면서 스토브 업적 시스템에 연결을 했습니다. 만약 스팀이나 스토브에 본인의 업적시스템을 연동하고자한다면

위와 같은 인터페이스를 추가하면 됩니다.

 

감사합니다. 오랜만에 글을 써보네요 다음에는 콤보나 상점에 관한걸 한번 써보고자 합니다. 감사합니다.