본문 바로가기

유니티 개발 정보/프로그래밍

간단한 Observer Pattern for Unity 4.2

원문은 이곳에서 보실수 있습니다.



유니티 4.2버전에 대한 Simple Observer Pattern


이 글에 대한 유니티 패키지는 여기서 다운로드 가능하다.
만약 얼마동안 유니티로 개발을 했다면, 자신이 작성한 코드에서 다음과 같은 부분을 발견할 수 있을 것이다.

GameObject.Find("BigBoss").GetComponent<BossManager>().AddHealth(-20F);


이와 같은 코드는 완전히 실용적임에도 불구하고, 나는 당신에게 이렇게 사용하지 말라고 제안할 것이다.
여기 그에 대한 몇가지 이유가 있다:
  1. 만약 BigBoss의 BossManager 스크립트를 일반적인 EnemyManager 스크립트로 교체한다면, BossManager스크립트를 참조하는 모든 스크립트는 깨질 것이다.(심지어 EnemyManager 스크립트가 AddHealth()를 가지고 있더라도 말이다)

  2. BigBoss의 체력 변화를 반드시 통보받아야하는 새로운 스크립트를 만들때 마다, BigBoss 체력변화를 수행하는 모든 스크립트를 수정해야한다.(새로운 스크립트에 체력 변경사항을 알리기 위해서)

  3. 만약 AddHealth() 함수가 UpdateState() 함수로 변경된다면, AddHealth()함수를 참조하는 모든 스크립트는 깨질 것이다.
다시 말해서, 이 방법은 클래스 사이에 강한 결합을 만든다. 이 방법을 사용한다면, 각 클래스는 반드시 접근하려는 클래스에 대한 밀접한 정보를 가지고 있어야 한다. 당신의 프로젝트가 점점 커질수록, 이는 많은 시간 낭비, 불필요하게 복잡한 코드, 그리고 융통성없는 프레임워크를 초래할 것이다.
이 문제에 대해 한 가지 가능한 해결책은 당신의 프로젝트에 Observer Pattern을 구현하는 것이다. 일반적으로 이 같은 종류의 시스템은 Events, Delegates, 그리고 Generics와 함께 사용될 수 있다. 그러나 대부분의 인디 개발자들은 이 패턴에 대한 더 난해한 공식 표현들과 씨름 할 것이다. 새로운 것을 배우는 것이 좋긴하지만, 때로는 단순히 당신의 게임을 끝내고 싶을 것이다. 이 글은 당신의 클래스에 다음의 것들을 적용할 수 있는 극도로 단순한 Observer Pattern을 보여줄 것이다.
  1. 손쉽게 이벤트를 발행하고, 구독한다.
  2. 다른 클래스와의 결합을 감소시킨다.
  3. 발행하고 구독할 수 있는 이벤트의 형태와 개수를 손쉽게 확장한다.
아직 프로젝트를 얻지 못하였다면, 여기서 이 글에 대한 유니티 패키지를 얻을 수 있다.
그리고 MessageManager 스크립트를 열어라. 
무슨 일이 나고 있는지를 보여주는 동작 흐름 다이어그램으로 시작해보자(시간의 흐름은 위에서 아래로 흐른다.)


프로그램의 특정시점에, 클래스는 MessageManager를 사용하여 특정 이벤트를 등록할 수 있다.
다른 시점에, 다른 클래스는 새로운 Message를 생성하고, 이 Message(이벤트에 대한 모든 정보)를 MessageManager로 보낼 수 있다. MessageManager은 방향을 바꿔 이 이벤트들를 등록한 모든 클래스에게 전송한다.

MessageManager은  SendToListener() 메소드안에서 보여진것 처럼 약간의 LINQ코드를 통해서 이벤트가 어떤 클래스나 메소드에 보내져야 하는지 알고 있다.

public void SendToListeners(Message m){ foreach (var f in listeners.FindAll(l => l.ListenFor == m.MessageName)) { f.ForwardToObject.BroadcastMessage(f.ForwardToMethod,m,SendMessageOptions.DontRequireReceiver );

} }


이 해법에 주의해야 할 것이 하나 있다. 즉, 메시지를 발행하고, 구독하는 모든 클래스는 반드시 MessageManager 객체를 참조해야 한다. 그러나 하나의 "하드-코딩"된 참조를 하나 가지는 것은 수백개를 가지는 것보다 훨씬 나을 것이다.
손쉽게 이 요구사항을 이행할 수 있는 한가지 방법은 간단하게, 이벤트를 전송하거나 받는 모든 클래스를 MessageBehaviour 클래스의 자식으로 만드는 것이다. 우리는 "World"라는 이름을 가진 빈 객체를 만들고 MessageManager 스크립트를 이 객체에 첨부함으로써, 이 방법을 수행할 수 있다.

public class MessageBehaviour : MonoBehaviour { protected MessageManager Messenger; public void Start() {     Messenger = GameObject.Find("World").GetComponent();     OnStart(); } protected virtual void OnStart(){ } }


메시지를 발행하고, 구독하는 각 클래스는 MessageBehaviour 클래스를 상속받고, OnStart() 함수를 구현하면 된다.
public class BigBoss : MessageBehaviour {
  protected override void OnStart () {
    // put code here that you would normally put in Start()
  }
}

우리는 모든 Message클래스와 유사한 것을 할 것이다 - 각각 기본 Message클래스를 상속 받을 것이다.
이것은 여전히 우리의 상속 계층도를 아주 단순하게 유지할 것이다.

나는 메시지를 위한 전용 클래스를 사용하는 것을 좋아한다. 왜냐하면 "편의 함수"를 Message 자체에 포함시키기 때문이다. 나는 이 예제에서는 하지 않았지만, 특정 포맷의 Message정보를 필요로 하는 구독자를 상상할 수 있을 것이다. 예를 들어, 당신이 내부에 클래스안에 32-bit UINT로 나타나는 IP주소를 가지고 있다고 하자. 대신에 구독자들은 string으로 된 이 정보가 필요할 것이다 - 만약 당신의 메시지가 클래스라면, 간단하게 클래스안에 GetIPAddressString() 함수를 만들 수 있다. 이는 모든 구독자가 정확하게 같은 방법으로 데이터를 재가공하는 것을 보장한다.
당신이 볼 수 있는 것처럼,  우리는 MessageManager가 많은 다른 Message 클래스를 처리할 수 있게하는 방법이 필요하다. - 다른 것들은 수십개의 속성을 가지고 있는 반면에, 약간의 name/value 쌍만을 가질 것이다.
많은 각각의 Message 클래스가 이 시스템에 사용되도록 하기 위해서, 우리는 다시 상속을 사용한다.(마찬가지로 구성요소들은 잘 작동한다.). 우리는 기본 Message 클래스를 사용한다. 그리고 다른 모든 Message는 이 클래스를 상속 받는다. 우리는 게으른 프로그래머이기 때문에, 모든 Message의 자식 클래스에 getter와 setter을 재작성하는 것을 좋아하지 않는다. 우리는 다음과 같은 시그니쳐를 통해서 기본 생성자를 호출할 것이다. 우리는 이를 NewPlayerMessage의 정의에서 볼 수 있다:
public class NewPlayerMessage : Message {
 public int PlayerGUID {get; set;}
 public NewPlayerMessage(GameObject s,string n,string v, int p) : base(s,n,v)
 {
   PlayerGUID = p;
 }
}

이제 우리는 제네릭 Message 혹은 NewPlayerMessage를 MessageManager의 SendToListeners() 메소드에 보낼 수 있다.
이 시스템에 추가하기를 원하는 약간의 것은 다음과 같을 것이다.
(다시 말해서, 나는 최대한 간단하게 만들기 위해서 이들을 생략했다.)
  • 클래스가  더 이상 필요 없는 Message를 받는 것을 멈추게 하기 위한 구독 해제 함수.

  • 모든 Message가 전역-고유 ID를 가지도록 요구되는 자동 Message GUID 시스템 

  • 클래스가 보낸이에게 메시지를 받았음을 알리는 것이 가능한 답신 시스템
나는 당신의 다음 프로젝트에 이 Observer Pattern의 구현을 사용할 수 있기를 바란다.