3D 액션 RPG

Unity 3D 기반 액션 RPG. 추상 클래스 기반 스킬 시스템, 제네릭 Object Pool 동적 최적화, 싱글턴 매니저 아키텍처 설계.

2024.11.27 ~ 2024.12.12 (15일)|팀 프로젝트 (팀장 / 핵심 시스템 설계)
UnityC#Object PoolingDesign PatternsGame Architecture
1 / 4

개요

3D 액션 RPG

Unity 3D 기반 4인 팀 프로젝트. 실시간 전투, 스킬 조합, 레벨업, 장비 수집 등 RPG 핵심 루프를 구현한 액션 RPG 게임.

  • 개발 형태: 팀 프로젝트 (프로그래머 4명, 팀장)
  • 기간: 2024.11.27 ~ 2024.12.12 (15일)
  • 담당: 스킬 시스템 구조 설계, 제네릭 Object Pool, 게임 매니저 아키텍처

동기

4명의 프로그래머가 협업하는 3D 액션 RPG 프로젝트였다. 플레이어가 다양한 스킬과 무기를 조합해 몬스터와 전투하고, 경험치와 아이템으로 캐릭터를 성장시키는 구조를 만들어야 했다.

팀장으로서 핵심 과제는 두 가지였다. 첫째, 4명이 각자 맡은 스킬과 시스템을 독립적으로 개발할 수 있는 구조를 설계하는 것. 둘째, 반복적인 오브젝트 생성/파괴로 인한 런타임 성능 문제를 해결하는 것이었다.

접근

시스템을 세 축으로 나눠 설계했다.

첫째, 스킬 시스템. Skill 추상 클래스를 정의하고, 쿨타임/마나/애니메이션 등 공통 동작을 상위에 통합했다. 스킬 데이터, 로직, 스탯을 분리하여 팀원 각자가 새로운 스킬을 독립적으로 추가할 수 있는 구조를 만들었다.

둘째, Object Pool. 제네릭 기반 풀 매니저를 구현하고, 사용량에 따라 풀 크기를 동적으로 확장/축소하는 최적화를 적용했다. 미사용 오브젝트는 코루틴으로 자동 정리하여 메모리 효율을 유지했다.

셋째, 매니저 아키텍처. GameManager를 중심으로 PlayerManager, ItemManager, UIManager 등을 싱글턴으로 분리하고, 코루틴 기반 초기화 순서 제어로 매니저 간 의존성 문제를 해결했다.

게임 플레이
게임 플레이

핵심 구현

영역내용
스킬 시스템Skill 추상 클래스 기반 확장 구조, SkillData/SkillStat 분리, StatInitializer 배열 기반 초기값 관리
Object Pool제네릭 PoolManager, 사용량 기반 동적 크기 조절, 미사용 풀 자동 정리 코루틴
매니저 아키텍처제네릭 SingletonManager 추상 클래스, WaitUntil 기반 초기화 순서 제어

기술 스택

기술적용 영역
C# / Unity 2022.3게임 로직, 스킬 시스템, 매니저 아키텍처, Object Pool
제네릭 프로그래밍SingletonManager<T>, ObjectPool<T> 타입 안전 설계
코루틴 / WaitUntil매니저 초기화 순서 제어, 풀 자동 정리
ScriptableObject스킬 데이터 관리, 스탯 초기화 설정

문서 구성

문서내용
01-스킬-시스템Skill 추상 클래스 설계, 데이터/로직/스탯 분리, StatInitializer 트러블슈팅
02-게임-매니저-아키텍처싱글턴 매니저 구조, 초기화 순서 문제 해결, 설계 회고
03-오브젝트-풀-최적화제네릭 Object Pool, 동적 크기 조절, 자동 정리 메커니즘
2 / 4

스킬 시스템

추상 클래스 기반 확장 가능한 스킬 아키텍처

Skill 추상 클래스로 공통 동작을 통합하고, 데이터/로직/스탯을 분리하여 팀원 4명이 독립적으로 스킬을 추가할 수 있는 구조를 설계한 시스템.


문제

RPG 게임에서 스킬은 종류가 계속 늘어나는 요소다. 투사체 스킬, 범위 스킬, 버프 스킬 등 동작 방식이 전혀 다른 스킬들이 쿨타임, 마나 소모, 애니메이션 트리거 같은 공통 로직을 공유해야 했다. 4명의 프로그래머가 각자 스킬을 구현하는 상황에서, 공통 로직이 분리되어 있지 않으면 중복 코드와 일관성 문제가 발생한다.

또한 각 스킬의 스탯(데미지, 쿨타임, 마나 비용 등)이 레벨에 따라 성장해야 했고, 스탯 구조가 스킬 타입마다 달랐다. 투사체 스킬은 사거리와 속도가 필요하고, 범위 스킬은 반경과 지속시간이 필요한 식이다.

접근: 데이터/로직/스탯 3계층 분리

SkillManager가 모든 스킬 프리팹을 관리하고, SkillController가 플레이어가 보유한 스킬의 초기화, 장착, 사용, 레벨업을 처리하는 구조로 설계했다.

각 스킬은 Skill 추상 클래스를 상속받아 쿨타임 체크, 마나 소모, 애니메이션 트리거 등 공통 동작을 물려받고, 실제 발동 로직만 하위 클래스에서 정의한다. 스킬의 메타데이터(아이콘, ID)는 SkillData로, 수치 스탯은 SkillStat 계열 클래스로 분리했다.

이 구조에서 새 스킬을 추가하려면 Skill을 상속한 클래스 하나와 해당 스탯 클래스만 만들면 된다. 기존 스킬이나 공통 로직을 건드릴 필요가 없으므로, 팀원 간 충돌 없이 병렬 개발이 가능했다.

런타임 스킬 컴포넌트 구성
런타임 스킬 컴포넌트 구성

트러블슈팅: StatInitializer 초기값 오류

스킬별 Stat 클래스(ProjectileSkillStat, AreaSkillStat 등)에서 초기값이 잘못 적용되는 문제가 발생했다. 레벨 1 스킬의 데미지가 0으로 들어오거나, 성장 계수가 누락되는 현상이었다.

원인은 SkillStat의 InitializeStats 로직에서 StatInitializer 배열을 순회하며 스탯을 등록하는 과정에 있었다. 스탯 타입 enum과 실제 배열 인덱스 간 매핑이 어긋나면서, 특정 스킬 타입에서 값이 엉뚱한 슬롯에 할당되고 있었다.

StatInitializer 배열의 각 원소가 스탯 타입을 명시적으로 선언하도록 수정하고, 등록 시점에 타입별로 정확히 매핑되는지 검증하는 로직을 추가하여 해결했다. Inspector에서 스탯 타입과 값을 직접 확인할 수 있는 구조로 바뀌면서, 이후 스탯 설정 오류를 시각적으로 발견할 수 있게 되었다.

3 / 4

게임 매니저 아키텍처

제네릭 싱글턴 기반 매니저 시스템 설계

GameManager를 중심으로 기능별 매니저를 분리하고, 코루틴 기반 초기화 순서 제어로 매니저 간 의존성 문제를 해결한 아키텍처.


문제

RPG 게임의 런타임은 플레이어 생성, 아이템 관리, UI 초기화, 체크포인트 저장 등 여러 시스템이 동시에 동작한다. 초기에는 이 시스템들이 서로의 참조를 직접 들고 있었고, Unity의 Awake/Start 호출 순서가 보장되지 않기 때문에 특정 매니저가 아직 초기화되지 않은 다른 매니저를 참조하면서 NullReferenceException이 발생했다.

게임 흐름 제어(사망/리스폰, 체크포인트, 씬 전환)도 여러 스크립트에 분산되어 있어서, 상태 변경 시 누락되는 처리가 빈번했다.

접근: 제네릭 싱글턴 매니저

GameManager를 게임 상태와 흐름의 단일 진입점으로 설계했다. 플레이어 사망/리스폰, 체크포인트 저장/복원, 게임 초기화 같은 흐름 제어를 한 곳에서 관리한다.

PlayerManager, ItemManager, UIManager 등은 제네릭 SingletonManager<T> 추상 클래스를 상속받아, 타입 안전한 싱글턴 접근(PlayerManager.Instance)을 제공한다. 각 매니저는 자신의 책임 범위 내에서만 동작하고, DontDestroyOnLoad로 씬 전환에도 유지된다.

SingletonManager 추상 클래스
SingletonManager 추상 클래스

트러블슈팅: 초기화 순서 경합

매니저 간 초기화 순서 문제로 참조가 null이 되는 현상이 반복되었다. GameManager가 PlayerManager를 참조하는데, PlayerManager의 Awake가 아직 실행되지 않은 상태인 경우가 대표적이었다.

각 매니저의 InitializeRoutine을 코루틴으로 전환하고, WaitUntil을 사용하여 의존 대상이 준비될 때까지 대기하도록 수정했다. 예를 들어 GameManager는 WaitUntil(() => PlayerManager.Instance != null)을 거친 후에야 플레이어 참조를 설정한다.

GameManager 초기화 로직
GameManager 초기화 로직

회고: 싱글턴 남용의 한계

프로젝트를 마치고 보면, 여러 매니저에 싱글턴을 적용한 것이 의존성과 결합도를 높이는 결과를 낳았다. 어디서든 XXXManager.Instance로 접근할 수 있다 보니, 매니저 간 암묵적 의존이 코드 전반에 퍼졌다.

지금 다시 설계한다면 GameManager가 필요한 매니저 인스턴스를 직접 멤버로 보유하게 하여 전역 접근을 줄이고, 의존 관계를 명시적으로 드러내는 구조를 택할 것이다. 테스트 시 매니저를 교체하거나 격리하기 어렵다는 점도 싱글턴의 실질적인 단점으로 체감했다.

4 / 4

오브젝트 풀 최적화

제네릭 Object Pool의 동적 크기 조절과 자동 정리

프리팹별 사용량을 추적하여 풀 크기를 동적으로 최적화하고, 미사용 오브젝트를 자동 정리하는 메모리 효율적 오브젝트 풀 시스템.


문제

액션 RPG 특성상 스킬 이펙트, 투사체, 몬스터, 드롭 아이템 등 반복적으로 생성/파괴되는 오브젝트가 많았다. 매 프레임 Instantiate와 Destroy를 호출하면 GC 스파이크가 발생하고, 메모리 단편화가 누적되면서 프레임 드롭으로 이어졌다.

초기 구현에서는 풀 크기가 고정이었다. 전투 강도가 높은 구간에서 풀이 부족하면 예외가 발생했고, 한가한 구간에서는 불필요한 오브젝트가 메모리를 점유하고 있었다.

접근: 사용량 추적 기반 동적 풀

PoolManager가 프리팹별 ObjectPool을 Dictionary로 관리하고, 각 풀이 자신의 최대 사용량(maxUsed)을 런타임에 추적하는 구조를 설계했다. 풀에 오브젝트를 요청할 때 남은 것이 없으면 자동으로 추가 생성하여 풀 부족 예외를 제거했다.

핵심은 OptimizePoolSizes 메서드다. 각 풀의 최대 사용량과 기본 크기 중 큰 값을 최적 크기로 계산하고, 상한(MAX_POOL_SIZE)을 넘지 않도록 제한한다. 최적 크기보다 많이 남아있는 오브젝트는 Destroy하여 메모리를 회수한다.

Object Pool 크기 최적화 메서드
Object Pool 크기 최적화 메서드

미사용 풀의 자동 정리도 코루틴으로 구현했다. 일정 시간 동안 요청이 없는 풀을 감지하여 오브젝트를 단계적으로 해제하고, 풀 자체를 제거한다. 전투가 끝난 후 이펙트 풀이 불필요하게 메모리를 점유하는 상황을 방지하기 위한 것이다.

트러블슈팅: 반환 오브젝트 상태 오염

풀에서 꺼낸 오브젝트를 반환할 때, 이전 사용 시의 상태(위치, 회전, 활성화된 파티클 등)가 초기화되지 않는 문제가 있었다. 투사체가 이전 위치에서 잠깐 보였다 사라지거나, 이펙트가 중간 상태에서 재생되는 현상이 발생했다.

반환 시점에 오브젝트를 비활성화하고, 풀에서 다시 꺼낼 때 위치/회전 초기화와 컴포넌트 리셋을 강제하는 처리를 추가했다. 미등록 프리팹이 요청될 경우에도 자동으로 새 풀을 생성하여, 런타임 중 등록 누락으로 인한 예외를 제거했다.

3D 액션 RPG · 2024.11.27 ~ 2024.12.12 (15일)