본 글은 유니티에서 발간한 전자책 'Level up your programming with game programming patterns'을 정리한 글입니다. 원문은 다음 링크에서 확인하실 수 있습니다.
https://unity.com/kr/resources/level-up-your-code-with-game-programming-patterns
1. Observer pattern
게임에선 여러 가지 일이 발생할 수 있다. 적을 파괴할 때, 아이템을 수집할 때, 미션을 완료할 때 모두 다른 일이 발생한다.이를 위해 불필요한 의존성을 만들지 않고 어떤 객체가 다른 객체에게 통지할 수 있는 메커니즘이 필요할 것인데, 옵저버 패턴(observer pattern)은 이러한 문제를 해결하는 일반적인 방법이다. 이 패턴을 사용하면 객체들이 일대다 의존성을 사용하여 느슨하게 결합되어 통신할 수 있다. 한 객체가 상태를 변경하면 모든 종속 객체가 자동으로 알림을 받으며, 이는 라디오 타워가 여러 다른 청취자에게 방송하는 것(broadcast)과 유사하다.

이때 방송하는 객체를 subject라고 하며, 청취하는 다른 객체들을 observer라고 한다. subject는 observer를 실제로 알지 못하며, 신호를 받은 후 관찰자들이 무엇을 하는지 신경쓰지 않는다. observer는 subject에 의존하지만, observer들 스스로는 서로에 대해 알지 못한다.
2. Events
C#은 이미 이벤트라는 것을 사용하여 observer 패턴을 구현하므로 subject-observer 클래스를 따로 설계할 필요가 없다. 이벤트란 단순히 어떤 일이 발생했음을 나타내는 알림으로 다음과 같이 설명할 수 있다.
- 발행자(subject)는 델리게이트를 기반으로 이벤트를 생성하여 특정한 함수 시그니쳐(파라미터 형식)를 설정한다. 이벤트는 subject가 런타임에서 수행할 특정 작업이다(예: 데미지 입히기, 버튼 클릭 등).
- 구독자(observer)는 각각 이벤트 핸들러라고 하는 메서드를 만들어야 하며, 이는 델리게이트의 시그니쳐과 일치해야 한다.
- 각 observer의 이벤트 핸들러는 발행자의 이벤트에 구독한다. 필요한만큼 많은 observer가 구독에 참여할 수 있으며, 모든 observer는 이벤트가 발생할 때까지 기다린다.
- 발행자가 런타임에서 이벤트 발생을 신호하는 경우 이를 이벤트를 raise한다고 말한다. 이로써 구독자의 이벤트 핸들러가 호출되며, 이들은 자체적인 내부 로직을 실행한다.
이 방법으로 여러 컴포넌트가 한 subject의 이벤트에 반응할 수 있다. subject가 버튼을 클릭했다고 알리면 observer는 애니메이션이나 소리를 재생하거나, 컷씬을 트리거하거나 파일을 저장하는 등의 작업을 할 수 있다.
3. Example: Simple subject and observer
다음 클래스 다이어그램과 같은 단순한 예제를 만들어보자.

Subject는 다음과 같이 작성할 수 있을 것이다.

여기서는 게임 오브젝트에 스크립트를 보다 쉽게 연결하기 위해 MonoBehaviour를 상속하지만, 필수적인 것은 아니다. 또한 자체적으로 델리게이트를 정의하여 사용할 수 있지만, 대부분의 경우 System.Action을 사용하면 된다. 이벤트와 함께 매개변수를 전달해야 하는 경우 Action<T> 타입의 델리게이트를 사용하면 된다. ThingHappened는 실제 이벤트이며, DoThing 메서드에서 이를 호출한다.
이벤트를 수신하기 위해 Observer를 다음과 같이 작성할 수 있다.

여기서도 편의를 위해 Monobehaviour를 상속했지만 필수적인 것은 아니다. 이를 통해 해당 컴포넌트를 게임 오브젝트에 추가하고 인스펙터에서 subjectToObserver를 참조하여 ThingHappened 이벤트를 수신하도록 할 수 있다.
OnThingHappened 메서드에는 observer가 이벤트에 응답하여 실행하는 모든 로직이 포함될 수 있다. 일반적으로 개발자들은 이벤트 핸들러를 나타내기 위해 접두사 "On"을 추가한다.
이벤트 핸들러는 Awake 또는 Start에서 += 연산자를 사용하여 이벤트에 구독할 수 있다. 이에 따라 subject의 DoThing 메서드를 실행하는 경우 이벤트가 발생하고, 이후 observer의 OnThingHappened 이벤트 핸들러가 자동으로 호출되어 디버그 문을 출력한다.
참고로, observer의 이벤트 핸들러가 이벤트에 구독된 상태에서 런타임 중 observer를 제거하는 경우 해당 이벤트를 호출하면 오류가 발생할 수 있다. 따라서 MonoBehaviour의 OnDestroy 메서드에서 -= 연산자를 사용하여 이벤트에서 구독을 해지하는 것이 중요하다.
게임 플레이 중 발생하는 거의 모든 사항에 observer 패턴을 적용할 수 있다. 가령 플레이어가 적을 파괴하거나 아이템을 수집할 때마다 이벤트를 발생시키게 할 수 있다. 점수나 업적을 추적하는 통계 시스템이 필요한 경우 observer 패턴을 사용하여 원래 게임 플레이 코드에 영향을 주지 않고도 해당 시스템을 만들 수 있다.
이벤트 적용의 흔한 케이스는 다음과 같은 것들이 있다.
- 목표
- 승리/패배 조건
- 플레이어 사망, 적 사망 또는 피해
- 아이템 획득
- 사용자 인터페이스
샘플 프로젝트에서 ButtonSubject는 사용자가 마우스 버튼을 클릭하여 Clicked 이벤트를 호출할 수 있게 한다. AudioObserver 및 ParticleSystemObserver 컴포넌트를 가진 다른 여러 게임 오브젝트들은 이 이벤트에 각각의 방식으로 응답한다.
https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/11%20Observer
어떤 객체가 subject이고 어떤 것이 observer인지는 사용 방식에 따라 다르다. 이벤트를 발생시키는 모든 것이 subject이고, 이벤트에 응답하는 모든 것이 observer이다. 동일한 게임 오브젝트의 다른 컴포넌트가 subject 또는 observer가 될 수 있으며, 심지어 같은 컴포넌트도 한 상황에서는 주체이고 다른 상황에서는 관찰자가 될 수 있다.
가령 샘플 프로젝트에선 AnimObserver는 버튼을 클릭할 때 약간의 모션을 추가하는데, 이는 게임 오브젝트 ButtonSubject의 일부이지만 observer로 작동한다.
4. UnityEvents and UnityActions
유니티는 UnityEngine.Events API에서 UnityAction 델리게이트를 사용하는 UnityEvent 시스템을 별도로 제공한다.
UnityEvents는 observer 패턴을 위한 그래픽 인터페이스를 제공하는데, 유니티의 UI 시스템을 사용해본 적이 있다면(UI 버튼의 OnClick 이벤트 생성 등) 이미 이를 경험해본 것이다.

위와 같이 설정했을 경우 버튼을 눌렀을 때 발생하는 OnClick 이벤트가 두 개의 AudioObserver의 OnThingHappened 메서드를 호출한다. 따라서 코드 없이도 subject의 이벤트를 설정할 수 있는 것이다.
UnityEvents를 사용하면 디자이너나 비프로그래머도 이벤트를 쉽게 설정하도록 할 수 있다. 그러나 System 네임스페이스에서 제공하는 이벤트나 동작을 사용하는 것보다 느릴 수 있다는 점을 염두에 두어야 한다.
5. Pros and cons
이벤트를 구현하면 몇 가지 추가 작업이 필요하지만 다음과 같은 이점이 있다.
- observer 패턴은 객체 간 결합도를 낮춰준다.
이벤트 발행자는 이벤트 구독자에 대해 알 필요가 없다. 한 클래스와 다른 클래스 사이에 직접적인 종속성을 만들기보다는 subject와 observer가 일정한 분리 상태를 유지하면서 통신한다.
- 직접 구현할 필요가 없다.
C#에는 정립된 이벤트 시스템이 포함되어 있으며, 커스텀 델리게이트를 정의하는 대신 System.Action 델리게이트를 사용할 수 있다. 또한 유니티에는 UnityEvents와 UnityActions도 포함되어 있다.
- 각 observer가 자체 이벤트 처리 로직을 구현한다.
이로 인해 디버깅 및 단위 테스트가 더 쉬워진다.
- 사용자 인터페이스에 적합하다.
이벤트를 통해 핵심 게임플레이 코드를 UI 로직과 별도로 유지할 수 있다. UI 요소는 그저 특정 게임 이벤트나 조건을 수신하고 적절하게 응답한다. MVP 및 MVC 패턴은 이러한 목적으로 observer 패턴을 사용한다.
observer 패턴의 주의사항은 다음과 같다:
- 추가 복잡성이 발생한다.
다른 패턴처럼, 이벤트 기반 아키텍처를 만들 때 초기 설정이 더 필요하다. 또한 subject 또는 observer를 삭제할 때 주의해야 한다. OnDestroy에서 observer를 등록 해제하는 것을 잊지 마라.
- observer는 이벤트를 정의하는 클래스에 대한 참조가 필요하다.
observer는 이벤트를 발행하는 클래스에 대한 종속성을 여전히 가지고 있다. 모든 이벤트를 처리하는 static EventManager(Improvements 참조)를 사용하여 객체 간 결합도를 낮출 수 있다.
- 성능 문제가 발생할 수 있다.
이벤트 기반 아키텍처는 추가 오버헤드를 발생시킨다. 이벤트를 사용하는 대규모 씬과 많은 게임 오브젝트는 성능에 영향을 줄 수 있다.
6. Improvements
여기에서는 observer 패턴의 기본적인 버전만 소개되었지만, 이를 확장하여 게임 애플리케이션의 모든 요구 사항을 처리할 수 있다. 이를 위해 다음과 같은 사항을 고려해볼 수 있다.
- C#에서 제공하는 ObservableCollection 클래스를 사용할 수 있다.
C#은 특정 변경 사항을 추적하는 동적인 ObservableCollection 클래스를 제공한다. 이를 사용하면 해당 집합에 항목이 추가되거나 제거되거나 목록이 새로 고쳐질 때 observer에게 알릴 수 있다.
- 고유한 인스턴스 ID를 인수로 전달할 수 있다.
하이라키의 각 게임 오브젝트에는 고유한 인스턴스 ID가 있다. 하나 이상의 observer에 적용될 수 있는 이벤트를 트리거하는 경우 Action<int> 타입을 사용하여 고유한 ID를 이벤트에 전달하고, 그것이 게임 오브젝트의 고유 ID와 일치하는 경우에만 이벤트 핸들러에서 로직을 실행할 수 있다.
- static EventManager를 생성하여 사용할 수 있다.
이벤트가 게임플레이의 많은 부분을 주도할 수 있기 때문에 많은 유니티 애플리케이션에서는 static 또는 싱글톤 EventManager를 사용한다. 이렇게 하면 observer가 이를 서브젝트로써 참조하여 이벤트를 더 쉽게 설정할 수 있다.
유니티 학습 프로젝트인 FPS Microgame에는 사용자 정의 GameEvents를 구현한 static EventManager의 좋은 구현이 있으며, 이에는 리스너를 추가하거나 제거하는 static 메서드가 포함되어 있다.
https://learn.unity.com/project/fps-maikeurogeim
- 이벤트 큐 생성
씬에 많은 오브젝트가 있는 경우 이벤트를 한 번에 발생시키고 싶지 않을 수 있다. 가령 한 이벤트를 발생시킬 때 천 개의 오브젝트가 소리를 재생하는 상황을 상상해봐라. 이를 해결하기 위해 observer 패턴을 command 패턴과 결합하여 이벤트를 이벤트 큐에 캡슐화할 수 있다. 그런 다음 command 버퍼를 사용하여 이벤트를 한 번에 하나씩 재생하거나 필요에 따라 선택적으로 무시할 수 있다(동시에 소리를 재생할 수 있는 객체의 최대 수가 있는 경우 등).
'Unity > Study' 카테고리의 다른 글
| Unity C#) 디자인 패턴 가이드 #8. MVP(Model-View-Presenter) (1) | 2024.03.22 |
|---|---|
| Unity C#) 디자인 패턴 가이드 #6. 스테이트 패턴(State pattern) (0) | 2024.03.17 |
| Unity C#) 디자인 패턴 가이드 #5. 커맨드 패턴(Command pattern) (0) | 2024.03.11 |
| Unity C#) 디자인 패턴 가이드 #4. 싱글톤 패턴(Singleton pattern) (3) | 2024.03.05 |
| Unity C#) 디자인 패턴 가이드 #3. 오브젝트 풀(Object pool) (1) | 2024.03.01 |