비주얼과 API 이벤트 레이어링: 확장 가능한 프로젝트를 위한 베스트 프랙티스 가이드
프로젝트가 성장하면서 가장 자주 받는 질문 중 하나는 *"비주얼 도구를 사용해야 하나요, 스크립팅 API를 사용해야 하나요?"*입니다. 답은 둘 다입니다. 하지만 어디에서 각 접근 방식이 빛을 발하는지 아는 것이 깔끔하고 확장 가능한 이벤트 아키텍처와 자체 무게로 무너지는 아키텍처를 가르는 핵심입니다.
다양한 규모의 팀이 GES를 사용하는 방식을 관찰한 경험을 바탕으로, 이벤트가 50개든 500개든 프로젝트의 유지보수성을 유지할 수 있는 레이어드 접근 방식을 공유하고자 합니다.
게임 오브젝트의 두 세계
모든 Unity 프로젝트에는 근본적으로 다른 두 가지 범주의 오브젝트가 있습니다.
씬 상주 오브젝트 — 편집 시점에 Hierarchy에 존재하는 것들. UI 캔버스, 레벨 지오메트리, 카메라, 영구 매니저, 환경 트리거. 보고, 선택하고, 참조를 드래그할 수 있습니다.
런타임 생성 오브젝트 — Instantiate()로 런타임에 생성되는 프리팹 인스턴스. 적, 투사체, 드롭 아이템, 풀링된 VFX, 동적으로 생성되는 UI 요소. 게임이 실행되기 전까지는 존재하지 않습니다.
이 구분이 이벤트 사용 레이어링의 기반입니다.
레이어 1: 씬 상주 오브젝트의 비주얼 설정
씬에 존재하는 모든 오브젝트에는 Editor, Behavior 윈도우, Flow Graph를 사용하세요.
왜일까요? 씬 상주 오브젝트에는 안정적인 Inspector 참조가 있기 때문입니다. UI Canvas에 있는 체력바, 레벨의 문 트리거, 배경 음악 컨트롤러 — 이 오브젝트들은 Hierarchy에 바로 거기 있습니다. 다음을 할 수 있습니다:
- Game Event Editor를 열고 이벤트를 찾아 Behavior 버튼 클릭
- Action Conditions를 비주얼로 설정 (예:
health < 30%일 때만 데미지 플래시 발동) - Schedule 타이밍 설정 (히트 사운드 0.2초 지연, 화면 흔들림 0.1초 간격으로 3회 반복)
- Hierarchy에서 직접 타겟 오브젝트를 드래그하여 UnityEvent actions 연결
- Flow Graph로 복잡한 시퀀스 구성 (화면 페이드 아웃 → 씬 로드 → 플레이어 재배치 → 화면 페이드 인)
이곳은 디자이너, 아티스트, 사운드 엔지니어가 최고의 역량을 발휘하는 영역입니다. 게임 필을 조정할 수 있습니다 — 딜레이를 0.2초에서 0.35초로 변경, 저체력 적에게 이펙트를 건너뛰는 조건 추가, Chain 시퀀스 순서 재배치 — 코드 한 줄 건드리지 않고, 리컴파일을 기다릴 필요도 없습니다.
적합한 용도
- UI 반응 — 버튼 클릭, 패널 전환, HUD 업데이트
- 레벨 스크립팅 — 문 열기, 함정 작동, 컷씬 트리거
- 오디오 이벤트 — 게임 상태에 따른 재생/정지/크로스페이드
- 카메라 동작 — 흔들림, 줌, 추적 대상 전환
- 환경 반응 — 조명 변화, 파티클 이펙트, 날씨 전환
- 튜토리얼 시퀀스 — 조건부 단계별 Chain 가이드
예시
OnPlayerDeath 이벤트로: 화면 어둡게, "You Died" 패널 표시, 사운드 재생, 플레이어 입력 비활성화. 네 가지 반응 모두 Hierarchy에 이미 존재하는 UI와 씬 오브젝트에 연결됩니다. Behavior 윈도우의 교과서적 사용 사례 — 네 개의 Action, 하나의 이벤트, 코드 제로. 디자이너가 나중에 패널 표시에 0.5초 딜레이를 추가하거나, 수중에서는 사운드를 건너뛰는 조건을 추가할 수 있습니다. 코드 변경 요청은 전혀 필요 없습니다.
레이어 2: 런타임 생성 인스턴스의 스크립팅 API
런타임에 생성되는 프리팹 인스턴스에는 AddListener(), RemoveListener(), Raise()를 사용하세요.
왜일까요? 프리팹을 인스턴스화할 때 참조를 드래그할 Inspector가 없기 때문입니다. 오브젝트 풀에서 방금 생성한 적은 OnPauseGame을 리슨하여 AI를 프리즈해야 합니다. 투사체는 타겟과 충돌할 때 OnEnemyHit을 발화해야 합니다. 이러한 바인딩은 코드에서, 오브젝트가 탄생하는 순간에 이루어져야 합니다.
public class Enemy : MonoBehaviour
{
[GameEventDropdown] public GameEvent onPauseGame;
[GameEventDropdown] public GameEvent onResumeGame;
void OnEnable()
{
onPauseGame.AddListener(Freeze);
onResumeGame.AddListener(Unfreeze);
}
void OnDisable()
{
onPauseGame.RemoveListener(Freeze);
onResumeGame.RemoveListener(Unfreeze);
}
void Freeze() => agent.isStopped = true;
void Unfreeze() => agent.isStopped = false;
}
중요한 점에 주목하세요: 이벤트 에셋 자체는 프리팹의 [GameEventDropdown] 속성을 통해 할당됩니다. 이벤트 참조는 비주얼입니다 — 프리팹 에셋의 드래그 앤 드롭 필드입니다. 리스너 등록만 코드입니다. 인스턴스가 편집 시점에는 존재하지 않기 때문입니다.
적합한 용도
- 생성된 적/NPC가 글로벌 이벤트에 반응 (일시정지, 슬로우 모션, 지역 효과)
- 투사체와 VFX가 충돌이나 수명 만료 시 이벤트 발화
- 풀링된 오브젝트의 활성/비활성 시 구독/구독 해제
- 동적으로 생성되는 UI 요소 (인벤토리 슬롯, 채팅 메시지, 리더보드 행)
- 편집 시점에 리스너 수를 알 수 없는 모든 시스템
예시
타워 디펜스 게임을 만들고 있다고 합시다. 타워는 런타임에 배치됩니다. 각 타워는 OnWaveStarted를 리슨하여 조준을 시작하고, OnWaveEnded로 대기 상태에 진입해야 합니다. 타워는 동적으로 인스턴스화되므로, 각 타워가 OnEnable()에서 자체 리스너를 등록하고 OnDisable()에서 정리합니다. 한편, OnWaveStarted를 발화하는 웨이브 매니저는 타이밍이 전부 Behavior 윈도우로 설정된 씬 상주 싱글톤일 수 있습니다.
레이어 3: 하이브리드 — 마법이 일어나는 곳
GES의 진정한 힘은 두 레이어를 의도적으로 결합할 때 나타납니다.
프로그래머가 이벤트 아키텍처를 정의하고, 런타임 시스템의 발화/리슨 코드를 작성합니다. 어떤 이벤트가 존재하는지, 어떤 데이터를 전달하는지, 언제 발화하는지를 결정합니다.
디자이너와 아티스트가 Behavior 윈도우, Flow Graph, Condition Tree를 사용하여 반응 내용을 설정합니다. 게임 필, 타이밍, 조건, 비주얼/오디오 마무리를 제어합니다.
구체적인 하이브리드 워크플로우:
[프로그래머가 작성]
- EnemyHealth.cs: 피격 시 OnEnemyDamaged(int damage) 발화
- EnemyHealth.cs: HP <= 0 시 OnEnemyDeath(GameObject enemy) 발화
- WaveManager.cs: 적 생성/소멸 시 리스너 동적 추가/제거
[디자이너가 Behavior 윈도우에서 설정]
- OnEnemyDamaged -> 데미지 숫자 UI 플래시, 카메라 흔들림 (조건: damage > 20)
- OnEnemyDeath -> 사망 VFX 재생, 점수 추가, 웨이브 완료 확인
[디자이너가 Flow Graph에서 설정]
- OnLastEnemyDeath -> OnWaveComplete 트리거
- OnWaveComplete -> 체인: 보상 패널 표시 -> 3초 대기 -> 다음 웨이브 생성
프로그래머는 화면 흔들림 지속 시간을 조정할 필요가 없습니다. 디자이너는 리스너 등록 코드를 작성할 필요가 없습니다. 각자 자신의 전문 영역에서 작업하며, 이벤트 에셋이 그들 사이의 공유 계약이 됩니다.
확장성을 위한 실전 가이드라인
프로젝트가 성장하면서 다음 원칙들을 기억하세요.
1. 오브젝트 수명이 선택을 가이드하게 하라
편집 시점에 씬에 있다 → 비주얼. 런타임에 인스턴스화 → API. 이 규칙 하나로 90%의 결정이 해결됩니다.
2. 코드에서도 이벤트 참조는 비주얼로 유지
MonoBehaviour 필드에 항상 [GameEventDropdown]을 사용하고, 이벤트 조회를 하드코딩하지 마세요. 프리팹에 타입 안전하고 검색 가능한 드롭다운이 제공되며, 코드 변경 없이 이벤트를 교체할 수 있습니다.
3. 반응 튜닝에는 Behavior 윈도우, 반응 로직에는 코드
반응이 "체력이 50% 이하일 때 0.3초 딜레이로 이 사운드 재생"이라면 설정 — Behavior 윈도우에. 반응이 "아머 타입, 속성 저항, 버프 스택에 따른 데미지 감소 계산"이라면 로직 — 코드로 작성.
4. 런타임 리스너 정리를 철저히
OnEnable() → 구독. OnDisable() → 구독 해제. 예외 없음. 오브젝트 풀링이나 빈번한 씬 로드가 있는 프로젝트에서 메모리 누수와 고스트 리스너를 방지하는 가장 중요한 습관입니다.
5. 실행 순서가 중요할 때는 우선순위 리스너 사용
여러 시스템이 같은 이벤트에 반응할 때, 등록 순서에 의존하지 마세요. AddPriorityListener()로 명시적 우선순위 값을 지정하세요. 데이터 저장은 우선순위 1000, 게임 상태 업데이트는 100, UI 갱신은 0, 오디오 재생은 -100. 실행 순서가 자체 문서화됩니다.
6. Flow Graph로 보이지 않는 관계를 가시화
이벤트가 Trigger나 Chain으로 다른 이벤트를 발화할 때, 반드시 Flow Graph에서 모델링하세요. 6개월 후, OnDoorOpened가 OnLightActivated, OnMusicChanged, OnTutorialStep3를 트리거한다는 것을 기억하는 사람은 없습니다. Flow Graph는 이러한 관계를 한눈에 파악할 수 있게 합니다.
7. 타입이 아닌 도메인으로 정리
이벤트 데이터베이스를 기술적 분류(Void 이벤트, Int 이벤트)가 아닌 게임 도메인(전투, UI, 오디오, 진행도)을 중심으로 구성하세요. 전투 디자이너가 무언가를 조정해야 할 때, 전투 관련 모든 것을 한 곳에서 찾을 수 있어야 합니다.
한눈에 보는 결정 가이드
| 레이어 | 접근 방식 | 담당자 | 시점 |
|---|---|---|---|
| 씬 오브젝트 | 비주얼 (Editor, Behavior, Flow Graph) | 디자이너, 아티스트, 사운드 | 편집 시 Hierarchy에 존재하는 오브젝트 |
| 런타임 인스턴스 | 스크립팅 API (AddListener, Raise) | 프로그래머 | 게임플레이 중 인스턴스화되는 프리팹 |
| 하이브리드 | 공유 계약으로서의 이벤트 | 모든 사람 | 프로그래머가 발화, 디자이너가 반응 |
핵심 정리
목표는 두 접근 방식 중 하나를 선택하는 것이 아닙니다. 목표는 각 팀원이 자신의 전문성에 맞는 도구로 작업하고, 이벤트 시스템이 각 분야 간의 깔끔한 경계 역할을 하는 것입니다.
아키텍처는 코드로 구축하라. 경험은 에디터로 다듬어라. 게임은 함께 출시하라.
