[Unity] Scriptable Object를 이용한 이벤트 처리

JongHa Shin (John)
Ward Games
Published in
6 min readSep 27, 2021

--

들어가며

안녕하세요! Ward Games에서 Software Engineer 로 일하고 있는 John입니다. 이번 포스팅에서는 Unity에서 Scriptable Object를 활용해 보다 효율적으로 C# 이벤트를 관리하는 방법을 소개하고자 합니다.

생산성에 영향을 미쳤던 Event 사용법

이벤트를 등록하기 위해서 이벤트 호출 객체를 참조하는 것의 문제점?

기존에 Zooports 프로젝트를 진행하면서 유니티 내의 이벤트들은 보통 스크립트를 통해 관리해왔습니다. 예를 들면 다음과 같습니다.

public class A : Monobehaviour
{
public UnityEvent someEvent;
}
public class B : Monobehaviour
{
public A eventCaller;

...

private void Start()
{
eventCaller.someEvent.AddListener(doSomething);
}

private void doSomething()
{
//Do Something
}
}

위와 같이 사용하거나 인스펙터상에서 UnityEvent를 가지고 있는 오브젝트를 찾아 직접 등록하는 방법도 있습니다. 하지만 이처럼 이벤트를 가지고 있는 객체를 직접 참조해 이벤트를 등록하고 사용하다보니 문제점이 있었습니다.

객체간 의존성이 강해진다.

프로젝트의 규모가 커지고 사용하는 클래스가 늘어날 수록 이벤트 때문에 각 클래스 간의 의존성이 강해진다는 문제가 있었습니다. 어떤 객체가 다른 객체의 호출하는 이벤트를 리슨하기 위해서는 결국 해당 객체에서 이벤트를 호출하는 객체를 알아야 할 수 밖에 없습니다. 인스펙터를 통해 이벤트를 등록해도 예외는 아닙니다. 리슨해야되는 이벤트가 많아지면 많아질 수록, 리슨해야되는 개체가 많아질 수록 의존성은 더욱 강해졌고 불필요한 코드 작업도 늘어나게 되었습니다. 또한 스크립트를 통해 이벤트를 리슨하려면 리슨하고자 하는 객체마다 해당 객체의 참조 변수 선언도 필요했습니다. 이와 같은 참조 변수 선언은 관리도 어려울 뿐더러, 리펙토링할 때도 많은 클래스가 영향을 받았습니다. 이는 결과적으로 생산성이 떨어지는 문제를 주게 되었습니다.

생산성을 높이는 Event 사용 방법

별도의 EventManager를 사용해 이벤트 관리하기

위와 같은 문제를 해결하기 위해서 간단하게 EventManager를 사용하는 방법이 있습니다. 이벤트를 모아놓은 별도의 Manager 클래스를 만들고, 이벤트를 호출하는 객체나 리슨하는 객체에서 해당 Manager 클래스를 통해 이벤트를 참조하면 됩니다.

public class EventManager
{
public static UnityEvent someEvent;
}
public class A
{
public void Invoke()
{
EventManager.someEvent.Invoke();
}
}
public class B
{
public void Listen()
{
EventManager.someEvent.AddListener(()=>{ //do something });
}
}

이 방법을 통해 의존성 문제를 해결할 수 있습니다. 하지만 개인적으로 저는 이러한 방법보다는 아래에서 설명할 Scriptable Object를 활용한 이벤트 관리 방법을 선호합니다. 이 방법에서 느낀 몇 가지 불편함이 있었서 그런데요.

  1. 이벤트를 호출하는 클래스내에 이벤트 변수가 존재하지 않는 점.
  2. 이벤트를 호출하는 객체와 리슨하는 객체 모두 스크립트의 특정 부분을 확인하지 않고서는 어떤 이벤트를 호출하고 리슨하는지 쉽게 파악하기 어렵다는 점.

위 2가지 이유 때문에 이벤트를 사용함에 있어서 의존성은 해결되었지만 가독성은 떨어진다는 느낌을 많이 받았습니다.

Scriptable Object를 활용해 이벤트 관리하기

Scriptable Object는 Unity Object에서 파생되었지만 Monobehaviour 와는 다르게 GameObject에 연결해서 사용하는 것이 아닌 별도의 에셋 형태로 사용되는 오브젝트입니다. 게임이 실행될 때 같이 메모리에 로드됩니다. 주로 런타임에 변경되지 않는 데이터를 저장하는 데에 사용하면 유용한 오브젝트인데요, 유니티 공식 블로그에서 확인한 바처럼 이벤트 관리에도 활용할 수 있었습니다.

Unity, 스크립터블 오브젝트로 게임을 설계하는 세 가지 방법

위 이미지처럼, 각 게임 오브젝트가 스크립터블 오브젝트를 통해 이벤트를 호출하고, 리슨할 수 있습니다. 위 예시에서 이벤트를 호출하는 주체인 Player는 플레이어가 죽었을 때 OnPlayerDied라는 Scriptable Object에 있는 이벤트를 호출합니다. 그리고 이 이벤트를 리슨하고 있던 다른 시스템들은 각자의 기능을 수행합니다. 즉 리슨하는 객체들은 Player를 몰라도 목적에 맞는 기능을 수행할 수 있으며 OnPlayerDied라는 Scriptable Object의 네임을 통해 어떤 상황에서 이벤트를 받는 지도 알 수 있습니다. 위에 나왔던 Manager를 활용한 방식과 유사하지만 재사용성, 가독성이 좋았습니다. 유니티 블로그에 나와있던 위 예시를 코드입니다.

GameEvent Scriptable Object

GameEventListener.cs

ZOOPORTS 에서 효율적으로 Scriptable Object와 이벤트를 사용했던 방법 (멀티플레이 커스터마이징 + 재사용)

위 코드를 활용해 ZOOPORTS 프로젝트에서는 멀티플레이 프로젝트에 맞게 서버, 클라이언트, 로컬의 상황마다 호출/등록 할 수 있는 이벤트와 리스너 클래스를 만들어 사용했습니다. 네트워크 상황에서 활용하기 때문에 이벤트 호출 상황 시 별도로 네트워크 데이터를 받아서 필터링 후 사용하는 것을 고민했는데 좀 더 범용적으로 사용하기 위해서 서버,클라이언트,로컬만 구분했습니다.

마치며

C#의 강력한 기능인 이벤트를 좀 더 효율적으로 사용하는 방법에 대해 소개해드렸습니다. Scriptable Object와 이벤트를 사용해보시지 않았다면 한 번 사용해보는 것도 생산성에 도움이 될 것이라 생각합니다.

읽어주셔서 감사합니다.

레퍼런스

https://unity.com/kr/how-to/architect-game-code-scriptable-objects

--

--