유니티

유니티 쉽게 배우는 디자인 패턴 - 옵저버 패턴 / Unity Design Pattern - Observer Pattern

고라니TV 2023. 10. 18. 13:21

 

 

디자인 패턴이란?

디자인 패턴이란 코드 구조를 설계하는 패턴입니다.

제일 중요한 건 디자인 패턴을 맹신하여 굳이 필요없는 부분에 강제로 쓰지 않는 것입니다.

왜 이 디자인패턴이 이 상황에 효율적인지 설명하지 못하면 실패한 설계입니다.

 

 

 

옵저버 패턴이란?

옵저버 패턴은 이벤트를 주체에서 발생시키고

관찰하는 대상은 주체를 구독함으로 실행 시점을 이벤트로 받아올 수 있습니다.

 

음악이 끝남을 알려주는 이벤트

광고를 다 봤음을 알려주는 이벤트

게임이 종료되었음을 알려주는 이벤트 등

많은 부분에서 이미 사용이 되고 있습니다.

 

 

UI의 버튼을 누르면 OnClick() 이벤트를 발생시켜 클릭할 때 변화를 알려주는 것에도 사용이 되고 있습니다.

 

 

 

 

 

GameManager 클래스에서 Moved라는 함수를 실행했다고 해봅시다.

Moved는 사용자의 입력이 있을때만 실행되기에 항상 실행되는게 아니라고 구상해봅시다.

 

이때 각 플레이어들이 GameManager의 Moved의 실행시점을 똑같이 이벤트를 받고 싶을때

GameManager안의 함수를 구독하면

GameManager에는 관찰하는 대상인 Player들은 모르지만 이벤트를 전달할 수 있게됩니다.

 

 

 

1. System.Action을 사용한 방법

GameManager.cs

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public event System.Action<float> Moved;

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        if (horizontal != 0) 
        {
            Moved?.Invoke(horizontal);
        }
    }
}

GameManager.cs에는 이벤트를 발생시키는 주체로 여기에 System.Action을 만듭니다.

event 키워드는 이 클래스 밖에서는 += 또는 -=으로 추가 제거만 할 수 있고 Invoke 실행은 할 수 없게 제한합니다.

매개변수로 값도 전달할 수 있습니다.

그리고 Update에서 키보드의 좌우키를 누를 때만 이벤트를 발생시킵니다.

?.을 한 이유는 Moved에 아무것도 구독한 게 없으면 함수를 실행시키지 않아 Null Reference 에러를 막기 위해서입니다.

 

 

Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] GameManager gameManager;

    void OnMoved(float horizontal) 
    {
        print($"움직임 {horizontal}");
    }

    void OnEnable()
    {
        gameManager.Moved += OnMoved;
    }

    void OnDisable()
    {
        if (gameManager) gameManager.Moved -= OnMoved;
    }
}

Player.cs에는 이벤트를 구독하는 대상으로 먼저 GameManager 객체를 참조합니다.

 

이 스크립트가 활성화 될 때 (OnEnable : Instantiate로 생성될 때도 포함됨) +=으로 이벤트 등록을,

이 스크립트가 비활성화 될 때 (OnDisable : Destroy로 파괴될 때도 포함됨)  -=으로 이벤트 해제를 합니다.

 

함수 이름을 지을 때는 구독됨이라고 알수 있도록 앞에 On을 붙힙니다.

 

 

하이어라키에 두 게임오브젝트를 만들고 각각의 스크립트를 넣습니다.

그리고 플레이해서 좌우 키보드 방향키를 누를 때만 로그가 출력됨을 알 수 있습니다.

 

그러면 GameManager가 Player를 알고있고 Player 객체를 참조하여 Moved함수를 전달하는 것에 비해 어떤 장점이 있을까요?

GameManager는 앞으로 일어날 등록 과정에 참조를 계속 할 일이 없어지게 됩니다.

 

 

 

2. delegate를 사용한 방법

GameManager.cs

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public delegate void delegateMoved(float horizontal);
    public event delegateMoved Moved;

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        if (horizontal != 0) 
        {
            Moved?.Invoke(horizontal);
        }
    }
}

GameManager만 delegate형태로 바꾸고 Player는 그대로입니다.

한 줄이 더 늘어났는데 어떤 장점이 있기에 이 방법도 소개할까요?

 

바로 구독하는 대상인 Player에서 구현할 때 += 이후 On함수를 작성하고 Ctrl + .을 눌러 메서드 생성부분을 선택하면 

자동으로 매개변수의 이름인 horizontal이 들어가 더 명확해지는 장점이 있습니다.

 

 

 

3. UnityEngine.Events.UnityAction을 사용한 방법

GameManager.cs

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public event UnityEngine.Events.UnityAction<float> Moved;

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        if (horizontal != 0) 
        {
            Moved?.Invoke(horizontal);
        }
    }
}

GameManager만 UnityAction으로 바꿨습니다.

UnityAction은 매개변수가 최대 4개까지 가능합니다.

 

 

 

마무리

이렇게 옵저버패턴을 살펴봤는데요.

1번방법에서 System을 using으로 빼도 되며, 3번 방법에서 UnityEngine.Events를 using으로 빼도 됩니다.

1, 2, 3 방법 중 취향껏 원하는 걸 사용하시면 됩니다.

참고로 저는 1번 방법을 제일 자주 사용해왔으나, 앞으로 개발시 2번 방법으로 나아갈까 고민하고 있습니다.