Abstract
이벤트 시스템은 "누가 듣는지 모르는 채로 알리는" 통로입니다. 적이 죽으면 점수가 오르고, HUD 가 바뀌고, 효과음이 나야 하는데 — 적 스크립트가 점수 매니저·HUD·사운드를 전부 알고 직접 부르면 서로 얽혀서 떼어 낼 수 없게 됩니다. 적은 "죽었다"고 외치기만 하고, 관심 있는 쪽이 각자 듣습니다.
실무에서 필요한 순간
•
한 가지 사건에 여러 시스템이 반응해야 할 때 (사망 → 점수, UI, 사운드, 업적)
•
UI 가 게임 로직을 참조하지 않게 하고 싶을 때. HUD 는 Player 클래스를 몰라도 체력 바를 그릴 수 있다
•
프레임워크의 비동기 결과를 받을 때 — ShowEntity, OpenUIForm, 다운로드, 웹 요청의 성공·실패는 전부 이벤트로 온다
ToyboxNightmare 의 이벤트 지도
이벤트 | 발행하는 쪽 | 듣는 쪽 |
CharacterSelectedEventArgs | PlayerSelectLogic (클릭된 캐릭터) | SurvivalGame (플레이어 스폰), HUDForm (안내 문구를 점수 표시로 전환) |
EnemyDiedEventArgs | Enemy | SurvivalGame (점수 가산) |
ScoreChangedEventArgs | SurvivalGame | HUDForm (점수 표시) |
PlayerHealthChangedEventArgs | Player | HUDForm (체력 바, 피격 플래시) |
WeaponCooldownStartedEventArgs | WeaponLoadout | HUDForm (쿨다운 게이지) |
AllyAvailabilityChangedEventArgs | AllySquad | HUDForm (아군 버튼 표시 / 숨김) |
PlayerDiedEventArgs | Player | SurvivalGame (게임오버 판정), HUDForm (Game Over 표시) |
HUDForm 은 여섯 가지 이벤트를 듣지만 Player, Enemy, SurvivalGame 중 어느 것도 참조하지 않습니다. HUD 프리팹을 통째로 다른 것으로 바꿔도 게임 로직은 한 줄도 고칠 필요가 없습니다.
세 단계로 쓰는 법
① 이벤트 인자를 정의한다
public class ScoreChangedEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(ScoreChangedEventArgs).GetHashCode();
public override int Id => EventId;
public int Score { get; private set; }
public int Delta { get; private set; }
public static ScoreChangedEventArgs Create(int score, int delta)
{
var args = ReferencePool.Acquire<ScoreChangedEventArgs>();
args.Score = score;
args.Delta = delta;
return args;
}
public override void Clear() { Score = 0; Delta = 0; }
}
C#
복사
② 발행한다
events.Fire(this, ScoreChangedEventArgs.Create(Score, delta));
C#
복사
③ 구독하고, 반드시 해제한다
// HUDForm.OnOpen
events.Subscribe(ScoreChangedEventArgs.EventId, OnScoreChanged);
// HUDForm.OnClose
events.Unsubscribe(ScoreChangedEventArgs.EventId, OnScoreChanged);
private void OnScoreChanged(object sender, GameEventArgs e)
{
ScoreChangedEventArgs ne = e as ScoreChangedEventArgs;
if (ne == null)
return;
mInfoText.text = string.Format(ScoreFormat, ne.Score);
}
C#
복사
Fire 와 FireNow
Fire | FireNow | |
배달 시점 | 다음 프레임 맨 앞 (이벤트 모듈이 가장 먼저 갱신된다) | 호출한 그 자리에서 즉시 |
스레드 | 아무 스레드에서나 가능 | 메인 스레드에서만 |
쓰는 곳 | 기본값. 예제는 전부 이것 | 같은 프레임 안에 결과가 반영돼야 할 때만 |
Fire 가 한 프레임 늦는 것은 결함이 아니라 안전장치입니다. 핸들러가 엔티티를 숨기거나 새로 스폰해도, 그것이 "엔티티 목록을 순회하는 도중"에 일어나지 않습니다.
자주 걸리는 곳
•
같은 (이벤트, 핸들러) 를 두 번 Subscribe 하면 예외, 구독하지 않은 것을 Unsubscribe 해도 예외입니다. OnOpen
OnClose, Initialize
Shutdown 처럼 반드시 짝이 맞는 곳에 둡니다.
OnClose, Initialize
Shutdown 처럼 반드시 짝이 맞는 곳에 둡니다.•
Unsubscribe 를 빠뜨리면 닫힌 UI 나 회수된 엔티티의 핸들러가 계속 불립니다. 파괴된 오브젝트에 접근해 MissingReferenceException 이 나는 흔한 원인입니다.
•
핸들러 안에서 인자를 보관하지 마세요. 인자는 핸들러가 끝나면 풀로 돌아가 다른 이벤트에 재사용됩니다. 필요한 값만 복사합니다. 같은 이유로 직접 Release 하지도 않습니다.
•
이벤트 인자의 기반 클래스는 게임 코드에서는 GameEventArgs 입니다. 본문의 BaseEventArgs 는 그 부모이고, EventPool<T> 는 프레임워크 내부 부품이라 직접 만들 일이 없습니다. 게임 코드의 창구는 GameEntry.GetComponent<EventComponent>() 입니다.
•
EventId 를 typeof(T).GetHashCode() 로 만드는 것은 원작의 관례입니다. 이론상 충돌 가능성이 있으나 실무에서는 문제 된 적이 드뭅니다. 걱정되면 직접 상수를 부여해도 됩니다.
이벤트 시스템 (Event System)
•
관련 소스 파일
◦
GameFramework/Base/EventPool/EventPool.cs
◦
GameFramework/Entity/EntityManager.EntityGroup.cs
◦
GameFramework/Entity/EntityManager.EntityInfo.cs
◦
GameFramework/Entity/IEntityGroup.cs
◦
GameFramework/Entity/IEntityManager.cs
◦
GameFramework/UI/IUIGroup.cs
◦
GameFramework/UI/IUIManager.cs
◦
GameFramework/UI/UIManager.UIGroup.cs
이벤트 시스템은 타입-안전(type-safe)한 메시지 전달 프레임워크로, 서로 직접적인 의존성을 만들지 않고 컴포넌트 간 통신을 가능하게 한다.
퍼블리셔-서브스크라이버 패턴을 구현하여, 컴포넌트가 특정 이벤트 타입을 구독하고, 이벤트를 발행(fire)하며, 이벤트 발생 시 이를 처리할 수 있게 한다.
개요 (Overview)
이벤트 시스템은 이벤트를 처리하기 위한 중앙 메커니즘을 제공하며, 주요 특징은 다음과 같다:
•
스레드 안전(thread-safe)한 이벤트 발행과 지연 처리
•
즉시 이벤트 발행(동기 처리용)
•
이벤트 핸들러 등록 및 관리
•
특정 핸들러가 없을 때 기본 처리 제공
핵심 컴포넌트 (Core Components)
EventPool
EventPool<T> 은 이벤트 시스템의 중심 컴포넌트로, 이벤트 등록, 큐잉(queueing), 디스패칭(dispatching)을 관리한다.
EventPool<T>는 다음을 관리한다:
•
이벤트 ID로 인덱싱된 이벤트 핸들러의 멀티 딕셔너리
•
지연 처리를 위한 이벤트 큐
•
처리되지 않은 이벤트를 위한 기본 핸들러
EventPoolMode
Event Pool의 동작은 EventPoolMode 플래그로 제어된다:
모드 | 설명 |
Default | 기본 모드 (모든 핸들러 허용, 중복 금지, 핸들러 없음 시 예외 발생) |
AllowMultiHandler | 하나의 이벤트 타입에 대해 여러 이벤트 핸들러 허용 |
AllowDuplicateHandler | 동일한 핸들러를 여러 번 등록 가능 |
AllowNoHandler | 핸들러가 없을 때도 예외를 억제 |
이벤트 흐름 (Event Flow)
이벤트 등록 (Event Registration)
알림을 받으려면, 컴포넌트는 Event Pool에 핸들러를 등록해야 한다.
Subscribe 메서드를 사용하여 이벤트 ID와 델리게이트 핸들러를 전달해 구독할 수 있다.
이벤트 발행과 처리 (Event Firing and Processing)
이벤트 발행 방법은 두 가지이다:
1.
지연 발행(Fire): 스레드 안전하며, 다음 Update 주기에서 처리됨
2.
즉시 발행(FireNow): 즉시 처리되지만 스레드 안전하지 않음
Update 메서드 동안, 큐에 쌓인 모든 이벤트가 처리되고 등록된 핸들러로 디스패치된다.
이벤트 처리 (Event Handling)
이벤트가 처리될 때, 시스템은 해당 핸들러를 찾아 호출한다.
특정 핸들러가 없을 경우, 기본 핸들러와 설정 옵션에 따라 처리되지 않은 이벤트에 대한 대응이 가능하다.
프레임워크 컴포넌트와의 통합 (Integration with Framework Components)
이벤트 시스템은 GameFramework 전반에서 컴포넌트 간 통신에 사용된다. 대표적인 예는 다음과 같다:
Entity Manager 이벤트
엔티티 매니저는 엔티티 라이프사이클과 관련된 이벤트를 발생시킨다:
•
ShowEntitySuccess: 엔티티가 성공적으로 표시되었을 때
•
ShowEntityFailure: 엔티티 표시가 실패했을 때
•
ShowEntityUpdate: 엔티티 로딩 과정 중
•
ShowEntityDependencyAsset: 엔티티 의존 에셋 로딩 시
•
HideEntityComplete: 엔티티가 숨겨졌을 때
UI Manager 이벤트
UI 매니저는 UI 폼 동작을 이벤트로 전달한다:
•
OpenUIFormSuccess: UI 폼이 성공적으로 열렸을 때
•
OpenUIFormFailure: UI 폼 열기에 실패했을 때
•
OpenUIFormUpdate: UI 폼 로딩 과정 중
•
OpenUIFormDependencyAsset: UI 폼 의존 에셋 로딩 시
•
CloseUIFormComplete: UI 폼이 닫혔을 때
베스트 프랙티스 (Best Practices)
Fire vs FireNow 사용 시점
•
Fire: 스레드 안전한 이벤트 디스패치가 필요할 때 (특히 메인 스레드가 아닌 경우)
•
FireNow: 즉시 처리가 필요하고 이미 메인 스레드에서 실행 중일 때
이벤트 핸들러 관리
•
불필요한 핸들러는 반드시 해제(unsubscribe)하여 메모리 누수를 방지할 것
•
이벤트 풀 모드를 고려하여 설계할 것:
◦
AllowMultiHandler: 여러 수신자가 필요한 브로드캐스트 이벤트용
◦
AllowNoHandler: 핸들러가 없어도 되는 선택적 이벤트용
스레드 안전 고려 사항
•
Fire 는 스레드 안전하지만, 이벤트는 다음 update 주기에서 처리됨
•
FireNow 는 스레드 안전하지 않지만 즉시 처리됨
•
Event Pool의 Update 메서드는 반드시 메인 스레드에서 호출되어야 함
중요한 주의사항 (Important Notes)
•
Fire 로 큐잉된 이벤트는 즉시 처리되지 않고, 다음 Update 호출 시 처리된다
•
EventPool 는 제네릭으로, 이벤트 인자는 반드시 BaseEventArgs 를 상속해야 한다
•
이벤트 인자(그리고 큐에 넣을 때 쓰는 내부 노드)는 GC 부담을 줄이기 위해 레퍼런스 풀로 관리된다. 핸들러 호출이 끝나면 이벤트 풀이 인자를 자동으로 반납한다 (핸들러 자체는 풀링 대상이 아니다)
•
게임 전역 이벤트(EventManager)는 AllowNoHandler | AllowMultiHandler 모드로 돈다. 구독자가 없어도 예외가 아니고, 한 이벤트에 여러 핸들러를 붙일 수 있지만, 같은 핸들러를 두 번 구독하면 예외다








