오늘은 인런 중에 증강 카드를 제시하고, 플레이어가 하나를 골라 캐릭터에 실제 효과를 적용하는 흐름을 전체적으로 붙였다.
처음에는 단순히 "카드 3장 띄우고 하나 선택" 정도로 생각했는데, 막상 구현하다 보니 데이터 정의, 추첨 로직, 적용 방식, 입력 처리, UI 반영까지 전부 이어져 있어야 자연스럽게 동작했다.
그래서 이번 작업은 기능 하나를 추가했다기보다는, 증강 시스템의 기본 뼈대를 세운 작업에 가까웠다.
전체 흐름은 다음처럼 이어진다.
예전에는 증강 관련 입력을 별도 IMC로 스위칭하는 방식이었는데, 이번에는 그 구조를 걷어내고 gameplay 입력 위에 자연스럽게 얹히는 비차단 오버레이 방식으로 바꿨다.
덕분에 "증강 UI를 열었기 때문에 입력 체계를 갈아끼운다"는 느낌보다, 게임 진행 중 필요한 순간에 UI가 올라오는 구조에 더 가까워졌다.
이번 작업에서 먼저 잡은 것은 증강을 어떤 형태로 정의하고 뽑을지에 대한 데이터 구조였다.
단순히 카드 이름만 두는 수준이 아니라, 증강의 실제 적용 방식과 추첨 풀의 역할을 분리해서 설계했다.
NSAugmentTypes: 증강 시스템 전반에서 공통으로 사용하는 타입/분류 성격의 기반NSAugmentDefinition: 개별 증강 하나를 설명하는 정의 데이터, 실제 카드 단위NSAugmentPoolDefinition: 어떤 증강들이 어떤 규칙으로 추첨될지 관리하는 풀 데이터NSAugmentPoolDefinition에서는 풀 태그로 이 풀이 일반 풀인지, 고등급 풀인지 구분할 수 있도록 했다.
또 Rarity Weights로 희귀도별 등장 확률을 조정할 수 있게 해서, 같은 시스템 안에서도 상황에 따라 다른 체감이 나오도록 만들었다.
Entries에는 실제 증강 Definition들을 넣고, Legendary Stat Entries에는 레전더리 기믹 변경 제한 이후에 등장할 수 있는 스택형 레전더리 수치 증강을 따로 넣는 구조로 정리했다.
이렇게 분리해 두니까 나중에 "어떤 상황에서 어떤 증강을 보여줄지"를 코드에서 하드코딩하지 않고 데이터로 제어하기 쉬워졌다.
특히 라이브 밸런싱이나 기획 변경이 들어왔을 때 대응하기 훨씬 편해질 것 같다.
증강을 실제로 보유하고 효과를 적용하는 책임은 PlayerState에 붙인 증강 인벤토리 컴포넌트가 맡도록 했다.
이 컴포넌트는 "플레이어가 지금 어떤 증강을 가지고 있는지"를 관리하면서, 선택된 증강을 실제 Ability System 쪽 효과로 변환해 붙여주는 역할을 한다.
적용 방식은 증강 종류에 따라 나뉜다.
StackEffectClass를 사용해서 GE를 적용한다.GrantedAbilityClass를 사용해서 GA를 부여한다.ClearAll로 한 번에 정리되도록 했다.즉, "능력 자체가 추가되는 증강"과 "기존 능력치를 누적해서 강화하는 증강"을 적용 레벨에서 분리한 셈이다.
이 구분을 해두니 레전더리처럼 구조가 큰 증강과 일반 수치형 증강을 같은 시스템 안에서 무리 없이 다룰 수 있었다.
증강 선택 컴포넌트는 플레이어에게 지금 어떤 카드 3장을 보여줄지 결정하는 쪽에 집중했다.
단순 랜덤이 아니라 희귀도 가중치, 풀 구성, 현재 쌓여 있는 대기 오퍼 수까지 고려해서 오퍼를 적재하는 구조다.
핵심은 PoolQueue와 PendingCount를 기반으로 오퍼를 관리하는 점이다.
그래서 한 번에 다 띄우는 방식이 아니라, 이벤트가 여러 번 발생하면 오퍼가 순서대로 쌓였다가 플레이어가 하나씩 처리할 수 있다.
리롤도 같은 흐름 안에 넣어서, 현재 제시된 카드 구성을 다시 뽑도록 연결했다.
이번 구현으로 플레이어 입장에서는 "증강 기회가 들어오면 대기열에 쌓이고, 필요할 때 확인하고, 마음에 안 들면 다시 뽑을 수 있는" 흐름이 만들어졌다.
단순한 팝업보다 실제 게임 시스템처럼 느껴지게 된 부분이다.
입력 쪽도 이번에 꽤 크게 바뀌었다.
기존에는 증강 UI 전용 IMC로 전환하는 식이었는데, 이 방식은 순간적으로 맥락이 끊기거나 입력 체계가 갈라지는 느낌이 있었다.
그래서 이번에는 기존 증강 IMC 스위칭을 없애고, Gameplay IMC에 증강 관련 입력을 직접 연결했다.
연결한 입력은 다음과 같다.
이 구조의 장점은 입력 흐름이 더 단순해졌다는 점이다.
UI가 떠 있어도 "지금 입력 컨텍스트가 바뀌었는지"를 따로 추적할 필요가 줄었고, 필요한 경우 UI 열림 여부만 보고 게이팅하면 된다.
특히 IsAugmentationPanelOpen() 같은 함수가 추가되면서 InputBinder 쪽 제어도 더 명확해졌다.
이번 작업은 로직만 붙인 것이 아니라, 플레이어가 증강 시스템을 실제로 체감할 수 있도록 UI도 함께 정리했다.
증강 카드 위젯, Augmentation 패널, HUD의 보유 증강 아이콘 표시까지 전체적으로 이어 붙였다.
눈에 띄는 변화는 두 가지였다.
이 덕분에 "증강 선택 UI"와 "보유 증강을 확인하는 패널"의 역할이 조금 더 명확해졌다.
카드는 이벤트성 UI에 가깝고, 패널은 현재 상태를 확인하는 UI에 가깝게 분리된 셈이다.
보유 증강 아이콘도 표시 방식이 조금 달라졌다.
NSAugmentationWidget.cpp의 OnOwnedIconsLoaded()에서는 아이콘을 WrapBox에 바로 넣지 않고 USizeBox로 한 번 감싸서 추가하도록 했다.
그래서 원본 텍스처 해상도와 관계없이 항상 일정한 크기로 보이도록 강제할 수 있고, 기본값은 OwnedIconSize 48x48이며 BP별로 EditDefaultsOnly에서 조정 가능하다.
개인적으로는 이 부분이 꽤 중요했다.
아이콘 에셋마다 원본 크기가 다르면 HUD가 쉽게 지저분해지는데, 표시 크기를 강제로 고정해 두면 데이터가 조금 들쭉날쭉해도 화면 품질은 안정적으로 유지되기 때문이다.
현재 증강 시스템의 내부 뼈대와 UI 흐름은 붙어 있지만, 실제 게임 이벤트와의 연결은 아직 후속 작업으로 남아 있다.
즉, "증강 시스템 자체는 동작하지만, 언제 이 오퍼를 띄울지"에 대한 실전 트리거는 아직 다 연결된 상태는 아니다.
아직 미연결인 부분은 다음과 같다.
EnqueueOffer 호출Server_EnqueueOffer 호출지금은 인런 환경에서만 O 키 디버그 함수 Debug_EnqueueAugmentOffer로 오퍼를 띄워 테스트하고 있다.
대신 이 디버그 루트 덕분에 GE 적용, GA 부여, 큐 적재, 리롤, UI 반영까지 핵심 흐름 자체는 먼저 검증할 수 있었다.
이번 시스템은 코드만 넣는다고 완전히 동작하는 구조는 아니다.
증강 Definition 데이터 에셋에서 실제로 어떤 GE와 GA를 연결할지 에디터에서 지정해 줘야 최종 동작이 완성된다.
정리하면 다음 기준으로 연결하면 된다.
GrantedAbilityClass만 사용StackEffectClass만 사용즉, 데이터 에셋 설계와 에디터 세팅이 런타임 로직만큼 중요하다.
이 부분이 빠지면 코드상으로는 선택이 끝나도 실제 효과가 안 붙기 때문에, 테스트할 때 가장 먼저 확인해야 할 포인트 중 하나다.
이번에는 UI 함수 이름도 전반적으로 손봤다.
기존 이름은 단순히 Show/Hide 중심이라 "정말 패널을 여는 것인지", "카드 섹션만 다루는 것인지", "리롤 요청을 어디로 전달하는 것인지"가 덜 드러나는 편이었다.
그래서 함수 이름을 역할 중심으로 정리했다.
| 변경 전 | 변경 후 | 의미 |
|---|---|---|
ShowAugmentation() | OpenAugmentationPanel() | 증강 패널을 연다. 중복 오픈 방지 플래그도 함께 관리한다. |
HideAugmentation() | CloseAugmentationPanel() | 증강 패널을 닫는다. |
| - | IsAugmentationPanelOpen() | 패널이 현재 열려 있는지 확인한다. 입력 게이팅에 사용한다. |
| - | RequestRerollAugment() | 리롤 요청을 HUDWidget 쪽으로 전달한다. |
| 변경 전 | 변경 후 | 의미 |
|---|---|---|
ShowAugmentation() | OpenAugmentationPanel() | 내부적으로 AugmentationWidget의 OpenPanel() 호출로 연결한다. |
HideAugmentation() | CloseAugmentationPanel() | 내부적으로 ClosePanel() 호출로 연결한다. |
| - | RequestRerollAugment() | AugmentationWidget의 리롤 요청 함수로 전달한다. |
| 변경 전 | 변경 후 | 의미 |
|---|---|---|
ShowAugmentation() | OpenPanel() | 패널을 보여주고, 보유 증강 아이콘도 함께 갱신한다. |
HideAugmentation() | ClosePanel() | 패널을 숨기고, 진행 중이던 비동기 로드도 취소한다. |
NativeOnKeyDown() | 제거 | Enhanced Input으로 전환하면서 더 이상 필요 없어졌다. |
| - | ShowCardSection() / HideCardSection() | 패널 전체가 아니라 카드 선택 영역만 따로 제어한다. |
| - | HandleOfferPresented() | 오퍼가 들어오면 카드 생성과 아이콘 비동기 로드를 시작한다. |
| - | HandleOfferClosed() | 오퍼가 끝났을 때 카드 영역을 숨긴다. |
| - | HandlePendingCountChanged() | 대기 중인 오퍼 개수 뱃지를 갱신한다. |
| - | HandleInventoryChanged() | 보유 증강 변경 시 아이콘 목록을 다시 그린다. |
| - | PopulateOfferCards() | 오퍼 ID를 Definition으로 바꿔 카드 이름, 설명, 아이콘을 채운다. |
| - | OnIconsLoaded() / OnOwnedIconsLoaded() | 비동기 아이콘 로드 완료 후 실제 UI에 반영한다. |
| - | NativeDestruct() | 비동기 핸들 취소와 델리게이트 구독 해제를 맡는다. |
ConfirmAugmentSelection() | 실제 연결 예정 | 최종적으로 Server_Choose() 호출로 이어지도록 바꿀 예정이다. |
RequestRerollAugment() | 실제 연결 예정 | 최종적으로 Server_RerollCard() 호출로 이어지도록 바꿀 예정이다. |
| 구분 | 함수 | 의미 |
|---|---|---|
| 신규 | SetAugmentIcon() | 증강 아이콘 텍스처를 세팅하고, 아이콘이 없으면 Collapsed 처리한다. |
이렇게 이름을 바꿔 두니 함수만 봐도 역할이 더 잘 읽힌다.
특히 UI 코드는 나중에 다시 봤을 때 "이 함수가 화면 전체를 여는 건지, 일부 섹션만 다루는 건지"가 안 보이면 유지보수가 힘든데, 이번 정리는 그런 혼란을 줄이는 데 의미가 있었다.
현재 테스트 기준으로는 다음 항목들을 확인했다.
O 키를 누르면 증강 선택 UI를 띄울 수 있다.O를 여러 번 누르면 오퍼가 큐에 순서대로 적재된다.Tab으로 증강 창을 토글하고, 보유 중인 증강 아이콘을 확인할 수 있다.T 키로 리롤이 가능하다.아직 실전 트리거는 덜 붙었지만, "오퍼 생성 → 선택 → 적용 → UI 반영"이라는 핵심 루프는 이제 테스트 가능한 상태까지 올라왔다.
그래서 다음 작업은 시스템을 새로 만드는 단계보다는, 실제 게임 이벤트와 자연스럽게 연결하는 단계가 될 것 같다.
이번 작업은 카드 UI 하나 만든 느낌보다, 인런 보상 시스템의 기반을 깔아 둔 작업에 더 가까웠다.
데이터 정의, 추첨 로직, 적용 책임, 입력, UI가 다 따로 놀지 않고 한 흐름으로 이어지게 만드는 게 핵심이었다.
특히 좋았던 점은 적용 방식을 GE와 GA로 분리한 부분, 그리고 입력을 별도 IMC 전환이 아니라 오버레이 방식으로 정리한 부분이다.
이 두 군데를 먼저 정리해 둔 덕분에 이후 레벨업 보상이나 보스 보상 같은 실제 게임 이벤트를 붙일 때도 구조가 크게 흔들리지 않을 것 같다.
나중에 실제로 적용할때는 실제 인런 이벤트와 EnqueueOffer 연결, 서버 권한 흐름 점검, 그리고 카드 선택/리롤의 최종 RPC 연결까지 마무리하면 증강 시스템이 훨씬 완성도 있게 자리잡을 것 같다.
stable_sort라는 안정 정렬이 필요했다. 그냥 sort는 불완전 정렬이라서 순서 보장이 안될수가 있다. 그래서 순서가 보장되어야 하는 지금같은 문제에서는 stable_sort를 쓰자, 그리고 sort함수안에서 모든 조건을 다 if문으로 분기할 수 있는게 아니면 끝에 return false를 반드시 붙여주자