유니티 옵저버 패턴 정리 (디자인 패턴)

반응형

옵저버 패턴 정의

한 객체의 주체(Subject)의 상태가 변경되었을 때, 이를 의존하는 다른 옵저버(Observers)가 자동으로 통보를 받고 업데이트되도록 하는 디자인 패턴.

 

장점

  • 느슨한 결합: Subject와 Observer 간 의존성이 낮아, 객체 간 독립성이 높아집니다.
  • 자동 업데이트: Subject 상태 변경 시 Observer가 자동으로 반응.
  • 확장성: 새로운 Observer를 추가해도 Subject의 코드에 변경이 필요 없습니다.

단점

  • 복잡성 증가: 너무 많은 옵저버가 존재하면 관리가 어려워질 수 있습니다.
  • 예측 어려움: 의도치 않은 동작으로 디버깅이 까다로울 수 있음.

간단하게 이해하기

  • 예시로 A라는 버튼이 있다고 가정하자 이 버튼을 클릭했을 때 B 오브젝트에서 소리가 나야 하고 C오브젝트의 애니메이션이 재생 되어야 한다면 옵저버 패턴을 사용하면 된다
  • A 오브젝트는 Action 이벤트를 만들고 B,C 오브젝트에서는 Awake시점에 A에 접근 후 이벤트에 콜백함수를 등록만 해두면 된다. 아래 코드를 통해 확인해 보자

코드를 통해 이해하기

[Subject 이벤트 핸들러 생성]
주체는 아래와 같이 public event Action Clicked 처럼 이벤트 핸들러를 생성해놓기만 하면됩니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

namespace DesignPatterns.Observer
{
    [RequireComponent(typeof(Collider))]
    public class ButtonSubject: MonoBehaviour
    {
        public event Action Clicked;

        private Collider m_Collider;

        void Start()
        {
            m_Collider = GetComponent< Collider>();
        }

        public void ClickButton()
        {
            Clicked?.Invoke();
        }

        void Update()
        {
            CheckCollider();
        }

        private void CheckCollider()
        {
            // Check if the mouse left button is pressed over the collider
            if (Input.GetMouseButtonDown(0))
            {
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                RaycastHit hitInfo;

                if (Physics.Raycast(ray, out hitInfo, 100f))
                {
                    if (hitInfo.collider == this.m_Collider)
                    {
                        ClickButton();
                    }
                }
            }
        }
    }
}

 

[Observer 구독하기]

옵저버(Observer)는 위에서 서브젝트가 생성한 이벤트 핸들러를 구독합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.Observer
{
    [RequireComponent(typeof(AudioSource))]
    public class AudioObserver : MonoBehaviour
    {
        // dependency to observe
        [SerializeField] ButtonSubject subjectToObserve;
        [SerializeField] float delay = 0f;
        private AudioSource source;

        private void Awake()
        {
            source = GetComponent< AudioSource>();

            if (subjectToObserve != null)
            {
                subjectToObserve.Clicked += OnThingHappened;
            }
        }

        public void OnThingHappened()
        {
            StartCoroutine(PlayWithDelay());
        }

        IEnumerator PlayWithDelay()
        {
            yield return new WaitForSeconds(delay);
            source.Stop();
            source.Play();
        }

        private void OnDestroy()
        {
            // unsubscribe/deregister from the event if we destroy the object
            if (subjectToObserve != null)
            {
                subjectToObserve.Clicked -= OnThingHappened;
            }
        }
    }
}

 

[Observer 상황]

마찬가지로 옵저버 이다

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace DesignPatterns.Observer
{
    public class AnimObserver : MonoBehaviour
    {
        [SerializeField] Animation animClip;
        [SerializeField] ButtonSubject subjectToObserve;
        void Start()
        {
            if (subjectToObserve != null)
            {
                subjectToObserve.Clicked += OnThingHappened;
            }
        }

        private void OnThingHappened()
        {
            if (animClip != null)
            {
                animClip.Stop();
                animClip.Play();
            }
        }
    }
}

 

결론

  • 최종적으로 A 코드에서 Update -> CheckColider -> ClickButton 순서에 따라 Clicked?.Invoke 코드가 
    `인보크` 되면 Subject를 구독했던 Observer인 B의 소리와 C의 애니메이션이 실행된다.
  • 1:N 관계인 만큼 상태 변화에 따라 동작하는 객체 수 가 많은 경우 자주 사용 될 것 같다.
반응형