보통 프로젝트에서 Delegate가 가장 잘 맞는 자리는 한쪽이 결과를 만들고, 다른 쪽은 그 결과에 반응만 해야 하는 경우다.
대표적으로 UI -> Game Logic, Game Logic -> UI, Logic -> Logic 같은 방향에서 자주 쓰게 된다.
Delegate가 잘 맞는 자리
Delegate는 호출하는 쪽과 반응하는 쪽을 직접 연결하지 않아도 된다는 점이 핵심이다.
예를 들어 체력이 변했을 때를 생각해 보면, 체력 자체는 상태 컴포넌트가 들고 있고 UI는 그 결과를 화면에만 표시하면 된다.
이때 상태 컴포넌트가 UI를 직접 찾아가서 ProgressBar를 바꾸는 식으로 짜기 시작하면, 상태 로직과 UI가 바로 붙어 버린다.
반대로 상태 컴포넌트는 OnHealthChanged만 브로드캐스트하고, UI는 거기에 바인딩해서 숫자와 게이지를 갱신하면 역할이 깔끔하게 나뉜다.
이런 구조가 Delegate를 가장 쓰기 편하게 만든다.
UI와 Game Logic
UI -> Game Logic 방향에서도 Delegate는 자주 쓴다.
다만 여기서는 UI가 직접 게임 시스템을 조작하는 것보다, 의도를 알리는 신호를 보내는 쪽으로 두는 것이 더 안정적이다.
예를 들어 버튼을 눌렀을 때 UI가 PlayerCharacter, GameMode, AbilitySystemComponent를 전부 직접 알아야 한다면 UI가 너무 무거워진다.
이 경우 UI는 OnStartClicked, OnSkillSelected, OnInventoryClosed 같은 Delegate만 내보내고, 실제 게임 로직은 바깥에서 바인딩해서 처리하는 편이 좋다.
이렇게 두면 UI는 재사용하기 쉬워지고, 같은 위젯을 다른 모드나 다른 화면에서도 그대로 붙일 수 있다.
UI가 게임 시스템을 호출하는 구조보다, UI가 이벤트를 발행하고 외부가 반응하는 구조가 훨씬 덜 꼬인다.
Game Logic과 UI
게임 안에서 무언가 바뀌었을 때 UI가 반응해야 하는 경우에 쓰이기 좋다.
대표적인 예시는 이런 것들이다.
- 체력 변경
- 경험치 증가
- 라운드 시작
- 보스 등장
- 남은 시간 변경
- 탄약 수 변경
이런 값들은 게임 로직에서 결정되고, UI는 그 결과만 보여준다.
그래서 이 경우 UI는 값을 직접 조회만 반복하는 것보다, 로직 쪽 Delegate에 붙어 있다가 필요한 시점에만 갱신하는 편이 더 자연스럽다.
특히 Unreal에서는 UUserWidget이 여러 상태 소스를 계속 Polling하는 구조보다, 필요한 이벤트만 받아서 갱신하는 쪽이 읽기도 쉽고 관리도 편하다.
UI는 상태 저장소가 아니라 반응 계층이라는 기준으로 보면 Delegate가 더 잘 맞는다.
Logic과 Logic
- 경험치 지급
- 퀘스트 카운트 증가
- 웨이브 카운트 갱신
- 드랍 처리
- 사운드 재생
- 미니맵 제거
이걸 적 클래스가 전부 직접 처리하기 시작하면 클래스 하나가 너무 많은 책임을 갖게 된다.
반대로 적은 OnEnemyDead만 보내고, 각 시스템이 자기 책임에 맞게 바인딩해서 처리하면 흐름이 훨씬 깨끗해진다.
상태를 소유한 쪽
Delegate를 두기 가장 좋은 위치는 보통 상태를 실제로 소유하고 있는 객체다.
값을 계산하거나 보유하는 주체가 직접 브로드캐스트해야 흐름이 단순해진다.
예를 들어 체력은 Character보다 StateComponent가 들고 있다면, OnHealthChanged는 StateComponent에 있는 편이 낫다.
Round 진행 상태를 RoundManager가 들고 있다면, OnRoundStarted, OnRoundCleared도 그쪽에 있어야 자연스럽다.
이렇게 해야 이벤트 이름만 봐도 누가 이 상태를 책임지는지가 보인다.
반대로 상태는 A가 들고 있는데 Delegate는 B에 있으면, 값을 바꾸는 위치와 이벤트를 쏘는 위치가 갈라져서 나중에 디버깅이 어려워진다.
조정자 두기
Delegate가 많아지면, 모든 객체가 서로 직접 바인딩하는 구조는 또 복잡해질 수 있다.
그래서 실제 프로젝트에서는 중간에 조정자 역할을 하는 객체를 하나 두는 경우가 많다.
이 역할은 보통 이런 클래스들이 맡는다.
PlayerControllerHUDGameModeGameStateSubsystem- 전용 Manager 클래스
예를 들어 UI 위젯은 버튼 클릭 Delegate만 내보내고, PlayerController가 그걸 받아서 게임 로직으로 넘기는 구조가 있다.
혹은 RoundManager가 라운드 이벤트를 발행하고, HUD, EnemySpawner, Minimap, SoundManager가 각각 반응하게 둘 수도 있다.
이런 방식은 누가 누구를 바인딩하는가를 한 곳에 모아 주기 때문에 읽기 편하다.
특히 BeginPlay, OnPossess, Widget 초기화 시점처럼 바인딩 지점을 모아 두면 흐름을 따라가기 쉬워진다.
비동기 완료
비동기 로드, 네트워크 응답, 애니메이션 종료, 타이머 완료처럼 나중에 끝나는 작업은 Delegate와 잘 맞는다.
호출한 쪽은 완료 시점만 받으면 되고, 실제 완료 로직은 작업을 수행한 쪽이 책임지면 된다.
상태 전이
Idle -> Combat, Combat -> Dead, RoundStart -> RoundEnd처럼 상태가 바뀌는 순간을 알릴 때도 좋다.
상태를 소유한 쪽이 전이 결과만 브로드캐스트하면, 주변 시스템은 그 시점에만 반응하면 된다.
확장 포인트
처음에는 반응하는 시스템이 하나뿐이어도, 나중에 늘어날 가능성이 보이는 이벤트는 Delegate로 열어 두는 편이 좋다.
예를 들어 지금은 라운드 종료 시 UI만 바뀌더라도, 나중에는 보상, 통계, 연출, 사운드가 다 붙을 수 있다.
이때 처음부터 이벤트로 열어 두면 기능이 늘어나도 기존 구조를 크게 흔들지 않는다.
남용하면 꼬이는 경우
Delegate가 편하다고 해서 모든 호출을 이벤트로 바꾸면 오히려 흐름이 안 보일 수 있다.
특히 아래 경우는 직접 함수 호출이 더 나은 경우가 많다.
- 반드시 즉시 실행되어야 하는 핵심 로직
- 한 대상만 정확하게 호출하면 되는 경우
- 호출 순서가 매우 중요한 경우
예를 들어 무기 발사는 입력을 받은 뒤 즉시 한 객체가 처리해야 하는데, 이걸 괜히 이벤트 체인으로 풀면 흐름만 늘어진다.
Delegate는 직접 참조를 줄이는 데 강점이 있지만, 호출 경로를 숨겨 버리는 부작용도 있기 때문에 핵심 경로까지 전부 이벤트화하는 건 좋지 않다.
'TIL' 카테고리의 다른 글
| Text RPG 협업 5/21~ (0) | 2026.05.22 |
|---|---|
| 일주일 게임잼 제작 ~ 발표 (0) | 2026.05.21 |
| Muzzil 카메라, 미니맵 구현 (0) | 2026.05.15 |
| "Git LFS 한도 초과." "?에셋이 몇 갠데" "없어" (0) | 2026.05.14 |
| 5/13 팀 작업 시작, 캐릭터 이동, 게임 시스템 구조 (0) | 2026.05.13 |