
게임 코드에서 스크립터블 오브젝트를 이벤트 채널로 사용
이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.
애플리케이션의 서로 다른 시스템이 함께 작동하도록 하려면 어떻게 해야 하나요? 한 가지 일반적인 해결책은 이벤트를 사용하여 객체 간에 메시지를 보내는 것입니다. 계속 읽으면서 Unity 프로젝트에서 스크립터블 오브젝트를 이벤트 채널로 사용하는 방법을 알아보세요.
이 가이드는 전자책과 함께 제공되는 데모를 통해 Unity 개발자를 지원하기 위해 제작된 6개의 미니 가이드 시리즈 중 다섯 번째 가이드입니다, 스크립터블 오브젝트를 사용하여 Unity에서 모듈식 게임 아키텍처 만들기.
이 데모는 고전적인 공과 패들 아케이드 게임 메커니즘에서 영감을 얻은 것으로, 스크립터블 오브젝트가 테스트 가능하고 확장 가능하며 디자이너 친화적인 컴포넌트를 만드는 데 어떻게 도움이 되는지 보여줍니다.
전자책, 데모 프로젝트, 이 미니 가이드는 Unity 프로젝트에서 스크립터블 오브젝트 클래스와 함께 프로그래밍 디자인 패턴을 사용하기 위한 모범 사례를 제공합니다. 이러한 팁은 코드를 간소화하고 메모리 사용량을 줄이며 코드 재사용성을 높이는 데 도움이 될 수 있습니다.
이 시리즈에는 다음 문서가 포함되어 있습니다:
시작하기 전 중요 참고 사항
스크립터블 오브젝트 데모 프로젝트와 이 미니 가이드 시리즈를 살펴보기 전에 디자인 패턴의 핵심은 아이디어에 불과하다는 점을 기억하세요. 모든 상황에 적용되지는 않습니다. 이러한 기술을 통해 Unity 및 스크립터블 오브젝트로 작업하는 새로운 방법을 배울 수 있습니다.
각 패턴에는 장단점이 있습니다. 특정 프로젝트에 의미 있게 도움이 되는 것만 선택하세요. 디자이너가 Unity 에디터에 크게 의존하고 있나요? 스크립터블오브젝트 기반 패턴은 개발자와의 협업을 돕는 좋은 선택이 될 수 있습니다.
궁극적으로 가장 좋은 코드 아키텍처는 프로젝트와 팀에 맞는 아키텍처입니다.
느슨한 결합, 높은 응집력
애플리케이션에서 서로 다른 모듈이나 시스템을 구축할 때는 이를 "코드의 섬"으로 생각하면 도움이 되는 경우가 많습니다. 각 모듈에는 공통 목적을 위해 함께 작동하는 여러 컴포넌트 또는 게임 오브젝트가 있을 수 있습니다.
예를 들어 플레이어의 패들은 플레이어 입력을 해석하는 스크립트, 움직임이나 충돌을 처리하는 스크립트 등으로 구성될 수 있습니다. 이러한 부분들 간에 상호 의존성이 있는 경우 인스펙터를 사용하여 긴밀하게 연결할 수 있습니다.
그러나 다른 객체에 종속성을 추가할 때마다 약간의 위험이 수반된다는 점을 명심하세요. 가능하면 외부 객체와의 종속성을 최소화하는 것이 좋습니다. 모듈이나 시스템 외부의 사물과의 통신은 그렇게 직접적이지 않습니다.
패들 스크립트가 게임에서 공을 참조하도록 할 수 있지만, 이는 둘이 연결되어 있다는 의미입니다. 종속성으로 연결되면 한 종속성을 변경하면 다른 종속성에 영향을 미칠 수 있습니다.
이상적으로는 다른 것을 손상시키지 않고 애플리케이션의 일부를 수정할 수 있어야 합니다. 목표는 모듈을 내부적으로 응집력을 유지하되 외부적으로 분리하는 것입니다.
인스펙터에서 필수 참조가 누락된 경우 프로젝트의 NullRefChecker 클래스를 사용하여 정중하게 경고를 표시할 수 있습니다. 각 컴포넌트가 설정되거나 초기화된 후 어딘가(예: Awake)에서 정적 Validate 메서드를 호출하기만 하면 됩니다.
사용자 지정 옵션 속성을 추가하여 필드를 설정하지 않은 상태로 둘 수 있는지 확인을 무시합니다.
이벤트 사용
그렇다면 애플리케이션에서 이러한 서로 다른 시스템이 함께 작동하도록 하려면 어떻게 해야 할까요?
한 가지 해결책은 이벤트를 사용하여 객체 간에 메시지를 보내는 것입니다. 이벤트는 위 이미지에 시각화된 것처럼 방송사-듣는 사람 모델을 따릅니다.
여기서 수신 객체는 메서드를 호출하거나 프로퍼티를 직접 참조하는 대신 브로드캐스터의 이벤트에 가입합니다.
한 구성 요소에 대한 변경 사항은 다른 구성 요소에 미치는 영향이 적습니다. 코드를 수정하면 여전히 문제가 발생할 수 있지만 객체들이 서로 얽히지는 않습니다. 중간에 있는 이벤트는 두 이벤트 사이에 완충 역할을 합니다.
우리는 종종 이러한 방송사와 청취자 관계의 객체를 느슨하게 결합된 것으로 설명합니다.
이벤트와 옵저버 패턴에 대한 자세한 내용은 기술 전자책 ' 게임 프로그래밍 패턴으로 코드 레벨업'에서 확인할 수 있습니다.

중앙 집중식 이벤트 시스템
중앙 집중식 이벤트
위의 시나리오에서 생방송 자키는 신호를 전송하는 역할만 담당합니다. 어떤 객체가 수신 중인지 상관하지 않습니다.
그러나 OnEnable 및 OnDisable 메서드를 사용하여 위임자를 구독하고 구독을 취소하려면 청취자가 생방송 자키에 대해 어느 정도 알고 있어야 합니다.
이벤트를 정적 클래스로 이동하여 브로드캐스터와 리스너를 더욱 분리할 수 있습니다. 일반적인 "게임 이벤트" 클래스는 둘 사이에 추가적인 추상화 계층을 삽입하는 데 도움이 될 수 있습니다. 이를 통해 방송자와 청취자가 서로에 대해 직접 알지 못해도 서로를 연결할 수 있습니다.
이 예제에서는 단순화를 위해 정적 게임 이벤트 클래스를 사용하겠습니다. 하지만 실제 프로덕션 시나리오에서는 UIEvents, GameStateEvents, HealthEvents, InventoryEvents 등과 같이 기능별로 더 작고 전문화된 클래스로 나누는 것이 좋습니다.
예를 들어 애플리케이션 종료, UI 화면 표시 또는 장면 로딩을 위한 정적 이벤트를 만들 수 있습니다. 이러한 이벤트를 정적으로 만들면 애플리케이션의 어느 부분에서나 액세스할 수 있습니다.
예를 들어 아래 예시와 같이 게임 이벤트를 생성할 수 있습니다.
정적 게임 이벤트는 원래 방송사와 청취자 사이에 중개자로 위치합니다. 수신자 또는 발신자 중 한 쪽을 수정하면 다른 쪽에 영향을 미칠 가능성이 줄어듭니다.
따라서 코드를 업데이트하면 예상치 못한 부작용이 줄어듭니다. 이벤트 정의를 한 곳에 저장하면 관리도 더 쉬워집니다.
정적 게임 이벤트는 효과적이지만, 게임 디자이너가 접근하기 어려울 수 있습니다. 정적이므로 코드 내에서 정의해야 하며 에디터에서 기본적으로 직렬화할 수 없습니다.
보다 에디터 친화적인 시스템을 원한다면 스크립터블 오브젝트를 기반으로 이벤트를 구현하는 것을 고려해 보세요.
using UnityEngine;
using System;
public static class GameEvents
{
public static Action ExitApplication;
public static Action HomeScreenShown;
public static Action<float> LoadProgressUpdated;
}
이벤트 채널은 방송사와 청취자 사이에 신호를 중계합니다.
이벤트 채널 구성
스크립터블오브젝트 기반 이벤트는 정적 이벤트에 대한 그래픽 대안을 제공합니다. 둘 다 비슷한 기능을 제공하지만 스크립터블 오브젝트는 인스펙터에 표시되므로 디자이너에게 더 친숙한 경향이 있습니다.
방송사에서 청취자에게 신호를 전달하기 때문에 라디오 타워에서 송신하는 것과 유사한 '이벤트 채널'로 생각할 수 있습니다.
다음을 포함하는 스크립터블 오브젝트는 이벤트 채널로 작동할 수 있습니다:
- 델리게이트(예: UnityAction 또는 System.Action): 이렇게 하면 구독자에게 알림을 보내고 데이터를 매개변수로 전달합니다.
- 이벤트 모금 방식입니다: 이 퍼블릭 메서드는 델리게이트를 호출합니다.
게임 플레이의 다양한 측면을 결정하기 위해 이벤트 채널을 얼마든지 설정할 수 있습니다.
UnityAction과 System.Action은 모두 델리게이트입니다. 프로젝트에서 하나 또는 두 가지 유형을 모두 사용할 수 있습니다.
UnityAction은 아티스트 친화적인 환경을 제공합니다. 그렇지 않으면 System.Action 델리게이트를 사용하세요.
아래에서 프로젝트의 VoidEventChannelSO 예시를 확인할 수 있습니다. 이 이벤트는 매개 변수를 전달하지 않는 스크립터블 오브젝트 기반 이벤트입니다.
여기서는 OnEventRaised라는 이름의 UnityAction을 사용하고 공용 RaiseEventmethod를노출합니다.
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(menuName = "Events/Void Event Channel",
fileName = "VoidEventChannel")]
public class VoidEventChannelSO : DescriptionSO
{
[Tooltip("The action to perform")]
public UnityAction OnEventRaised;
public void RaiseEvent()
{
if (OnEventRaised != null)
OnEventRaised.Invoke();
}
}

프로젝트에서 이벤트 채널을 생성합니다.
이벤트 채널 자산 만들기
프로젝트에서 이벤트 채널 에셋을 생성하여 사용합니다. 생성 메뉴를 사용하거나 기존 에셋을 복제할 수 있습니다.
각 에셋의 이름을 바꾸고 설명 필드를 사용하여 각 스크립터블 오브젝트 에셋을 식별합니다. 각 이벤트 채널은 프로젝트 수준 에셋으로 존재한다는 점을 기억하세요. 모노비헤이비어에서 이러한 에셋을 참조하게 됩니다.
선택 사항이지만 스크립터블 객체 기반 이벤트 채널에 _SO 접미사를 붙여 데이터를 전달하는 다른 스크립터블 객체(_Data 접미사가 있는)와 구분할 수 있습니다.
폴더와 이름 지정 규칙을 사용하면 프로젝트를 체계적으로 관리할 수 있습니다. 프로젝트의 필요에 맞게 사용자 정의할 수 있습니다. 자세한 내용은 C# 스타일 가이드 만들기를 참조하세요.

인스펙터에서 이벤트 채널을 할당합니다.
모금 이벤트
이제 씬의 모든 오브젝트가 이벤트 채널을 참조하고 RaiseEvent 메서드를 사용하여 이벤트를 호출할 수 있습니다. 예를 들어, 아래의 TriggerEvent 메서드를 사용한 MonoBehaviour 샘플을 살펴보세요.
인스펙터에서 스크립터블 오브젝트 에셋을 m_EventChannel 필드에 할당해야 합니다. 무언가가 TriggerEvent를 호출하면 이벤트가 실행됩니다. 그러면 수신 중인 모든 항목에 알림이 전송됩니다.
이 메커니즘은 게임 애플리케이션에 많은 상호작용을 추가합니다. 각 모듈 또는 시스템은 이벤트를 발생시킵니다(예: 입력 시스템이 키 누름을 등록하거나 공이 벽에 충돌하는 등). 이에 대한 반응으로 다른 무언가가 반응합니다.
public class EventRaiser: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
public void TriggerEvent()
{
m_EventChannel.RaiseEvent();
}
}

게임 매니저는 특정 이벤트 채널과 다른 이벤트 채널의 방송을 청취합니다.
이벤트 듣기
리스너를 설정하려면 모노비헤이비어 또는 기타 컴포넌트가 이벤트 채널의 OnEventRaised 이벤트에 가입해야 합니다. 일반적으로 아래 예시에서와 같이OnEnable에서 발생합니다.
이벤트 채널이 이벤트를 발생시키면 핸들 이벤트 메서드가 응답으로 실행됩니다. 이 메커니즘은 이벤트의 상황에 따라 사운드나 효과 재생, 설정 수정 등 다양한 용도로 사용할 수 있습니다.
패들볼소 프로젝트에서 메인 게임 루프를 설정하는 방법은 다음과 같습니다. GameManager는 한 이벤트 채널 세트를 수신한 다음 다른 채널에서 브로드캐스트합니다. 이를 통해 서로 다른 시스템이 직접적인 종속성을 가지지 않고도 서로 메시지를 보낼 수 있습니다.
마지막으로, 오류나 메모리 누수를 방지하기 위해 OnDisable 메서드에서 OnEventRaised 이벤트의 구독을 취소합니다.
public class EventListener: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
private void OnEnable()
{
m_EventChannel.OnEventRaised += HandleEvent;
}
private void OnDisable()
{
m_EventChannel.OnEventRaised -= HandleEvent;
}
private void HandleEvent()
{
Debug.Log("Event received");
}
}

인스펙터에 설정된 코드 없는 상호 작용 기능
코드 없는 리스너 추가하기
디자이너와 함께 작업하는 경우 이벤트를 수신할 수 있는 미리 구성된 범용 스크립트를 디자이너에게 제공할 수 있습니다. 이를 통해 프로그래머 없이도 게임 상호작용을 만들 수 있습니다.
VoidEventChannelListener가 그 예시입니다. 이 컴포넌트는 이벤트 채널에서 신호를 수신하면 UnityEvent를 발생시킵니다. 게임 오브젝트에 VoidEventChannelListener를 추가한 다음 이벤트 채널과 UnityEvent 로직을 설정하기만 하면 됩니다.
그런 다음 디자이너는 인스펙터에서 몇 가지 설정만으로 이벤트 중심 로직을 프로토타입으로 만들 수 있습니다.
예를 들어 게임오버사운드 프리팹은 게임오버_SO 이벤트 채널을 수신합니다. 이 이벤트를 수신하면 m_Response UnityEvent를 통해 지정된 오디오소스에서 사운드를 재생합니다.
VoidEventChannelListener 클래스에는 각 응답의 타이밍을 조정하는 데 유용한 지연 기능도 포함되어 있습니다.
조금만 연습하면 서로 다른 시스템과 모듈 간에 상호작용을 구축할 수 있는 간단한 방법입니다.

송수신용으로 표시된 이벤트 채널
이벤트 채널의 지원 방법
이벤트 채널은 프로젝트 레벨에 존재하므로 전 세계에서 액세스할 수 있습니다. 이를 통해 씬 계층 구조의 모든 오브젝트를 연결하고 씬 로딩을 지속할 수 있습니다.
모든 개체는 브로드캐스터 또는 리스너 역할을 할 수 있으며, 이벤트 채널과 인터페이스하는 방법만 다를 뿐입니다. 이렇게 하면 메시지를 보낼 때 매우 유연하게 사용할 수 있습니다.
참고: 인스펙터에서 채널이 송신용인지 수신용인지 표시하는 것이 좋습니다. 이를 위해 HeaderAttribute를 사용합니다.
프로젝트 수준에서 이벤트를 사용하면 싱글톤의 필요성을 대체할 수 있는 경우가 많다는 이점이 있습니다. 이벤트 채널은 전 세계에서 사용할 수 있으므로 무엇이든 연결할 수 있습니다. 카메라, 퀘스트, 건강, 업적과 같은 게임 내 시스템을 불필요한 종속성 없이 구동할 수 있습니다.
또한 이벤트 기반 아키텍처는 필요할 때만 실행되기 때문에 모노비헤이비어의 업데이트 방식보다 더 최적화되어 있습니다.
기본 이벤트의 함수 시그니처
이 VoidEventChannelSO 클래스는 파라미터가 필요 없는 이벤트에만 작동합니다. 제기된 이벤트가 의미를 갖기 위해서는 추가적인 데이터 페이로드가 필요한 경우가 많습니다.
예를 들어, 생명력 시스템에 피해를 입히는 이벤트를 전송하는 경우 대상, 전송할 피해량, 피해 유형 등의 값을 전달할 수 있습니다.
기본 이벤트의 함수 시그니처를 변경하여 이벤트 채널에 더 적합하게 만들 수 있습니다. 이 프로젝트는 이를 위해 GenericEventChannelSO를 정의합니다. 아래 예시를 살펴보시기 바랍니다.
이것은 단일 일반 매개변수가 있는 추상 클래스입니다. 여기에서 다른 이벤트 채널을 파생할 수 있습니다. 그런 다음 플로트, 인티, 부울과 같은 단일 매개변수를 전달할 수 있습니다.
VoidEventChannelSO와 마찬가지로 GenericEventChannelSO에는 OnEventRaised라는 UnityAction이 있습니다. 그러나 이번에는 액션에 T 유형의 매개 변수가 전달됩니다.
외부 객체는 해당 공용 RaiseEvent 메서드를 호출합니다. 이벤트에 리스너가 있는 경우 지정된 매개변수를 전달하면서 실행됩니다.
public abstract class GenericEventChannelSO<T>: DescriptionSO
{
public UnityAction<T> OnEventRaised;
public void RaiseEvent(T parameter)
{
if (OnEventRaised == null)
return;
OnEventRaised.Invoke(parameter);
}
}
구체적인 이벤트 채널 만들기
이제 GenericEventChannelSO에서 구체적인 이벤트 채널을 도출하고 T의 값을 입력하기만 하면 됩니다.
일반적인 CreateAssetMenu 어트리뷰트 외에는 명시적인 구현 세부 사항이 필요하지 않습니다.
플로트를 전달하는 이벤트 채널인 FloatEventChannelSO를 만드는 방법은 간단합니다. 아래 코드 예시를 살펴보세요.
그렇게 간단합니다! 이 워크플로우를 사용하여 BoolEventChannelSO, IntEventChannelSO 등에 대한 워크플로우를 추가로 생성할 수 있습니다.
페이로드로 둘 이상의 파라미터가 필요한 경우, 필요에 따라 추가 제네릭 클래스(예: GenericEventChannelSO<T,U>, GenericEventChannelSO<T,U,V> 등)를 정의하세요.
[CreateAssetMenu(menuName = "Events/Float EventChannel", fileName = "FloatEventChannel")]
public class FloatEventChannelSO : GenericEventChannelSO<float> {}

공이 득점골에 맞았을 때의 이벤트 시퀀스
모든 것을 종합하기
이 아이디어는 애플리케이션을 더 작고 모듈화된 부분으로 나누는 것입니다. 명확한 경계를 설정하면 이러한 부분이 종속성으로 얽히지 않고 스파게티 코드를 방지하는 데 도움이 됩니다.
외부 객체에 대한 직접적인 지식이 없는 컴포넌트는 조작해서는 안 되는 것을 조작할 수 없습니다. 대신 이벤트 채널을 통해 메시지를 주고받아야 합니다.
패들볼 게임 플레이의 작은 시퀀스를 추적해 보면 어떻게 작동하는지 알 수 있습니다. 예를 들어 공이 ScoreGoal과 충돌할 때 어떤 일이 발생하는지 상상해 보겠습니다:
점수 목표 컴포넌트는 충돌을 등록합니다. 공을 감지한 후 GoalHit_SO 이벤트 채널에 이벤트를 발생시킵니다. 득점한 플레이어의 플레이어 ID를 전달합니다.
이벤트 채널은 게임 매니저에게 알리고, 게임 매니저는 이에 대한 응답으로 PointsScored_SO라는 또 다른 이벤트 채널을 발생시킵니다. 플레이어 ID도 전달합니다.
이 채널은 점수를 증가시키고(별도의 오브젝트에 저장) UI 컴포넌트를 업데이트하는 ScoreManager에 알림을 보냅니다. 그런 다음 ScoreManagerUpdated_SO 이벤트 채널을 통해 두 플레이어 점수를 모두 전달합니다.
이에 대한 응답으로 ScoreObjective_SO 목표는 한 플레이어가 목표 점수에 도달했는지 확인합니다.
승리 조건에 도달하면 게임이 종료됩니다. 그렇지 않으면 게임 매니저가 라운드를 리셋하고 공이 다시 플레이로 돌아갑니다.
언뜻 보기에 점수 값을 1점씩 올리는 것은 많은 추가 작업이 필요한 것처럼 보일 수 있습니다. 그러나 의도는 관련된 모든 부분을 격리하는 것입니다: 볼, 스코어매니저, 게임매니저, 목표매니저 등.
애플리케이션의 모든 부분에는 일정한 자율성이 있으므로 각 부분을 더 쉽게 테스트할 수 있습니다. 새로운 시스템을 추가한다고 해서 기존 로직을 중단할 필요는 없습니다. 사실, 원래의 게임 플레이를 완전히 잊어버릴 수도 있습니다.
채점 과정에 사운드 및 애니메이션과 같은 보조 효과를 추가하고 싶다고 상상해 보세요. 적절한 이벤트를 수신하고 적절하게 대응하는 새로운 컴포넌트를 만들 수 있습니다. 새로운 시스템을 추가하더라도 기본 로직과 게임 흐름은 방해받지 않고 그대로 유지할 수 있습니다.
SOLID 프로그래밍의 모토는 "확장을 위해서는 개방, 수정을 위해서는 폐쇄"라는 점을 기억하세요. 기존 코드를 변경하지 않고도 소프트웨어에 새로운 기능을 추가할 수 있는 기능을 원합니다. 이와 같은 이벤트 채널을 사용하면 확장성이 제공됩니다.

에디터 스크립팅을 사용하면 이벤트를 디버깅하는 데 도움이 됩니다.
이벤트 디버깅
이벤트 중심 아키텍처는 디버깅과 유지보수를 용이하게 합니다. Unity 테스트 프레임워크로 자동화된 유닛 테스트를 작성하든 비공식적인 문제 해결을 하든 작은 부품일수록 테스트하기가 더 쉽습니다. 이를 통해 특정 이슈에 집중하여 개별적으로 테스트할 수 있습니다.
여기서 사용자 지정 편집기 스크립팅이 도움이 될 수 있습니다. 패들볼소에서는 이벤트 채널을 사용할 때 애플리케이션의 흐름을 추적하는 데 도움이 되는 몇 가지 도구를 시연합니다:
- 패들볼소 프로젝트의 대부분의 이벤트 채널은 인스펙터에 리스너 목록을 표시합니다. 각 리스너의 이름을 클릭하여 계층 구조에서 강조 표시합니다.
- 커스텀 RaiseEvent 버튼으로 모의 이벤트를 마음대로 호출할 수 있습니다(페이로드를 전달하는 경우 기본값 T 사용). 애플리케이션이 실행 중일 때는 클릭 한 번으로 수동으로 트리거할 수 있습니다.
이벤트 채널 문제를 해결할 때 스크립터블 오브젝트 에셋을 선택합니다. 필요에 따라 수동으로 이벤트를 테스트합니다. 인스펙터는 청취할 수 있는 개체를 안내해 줍니다. 더 자세히 검사할 리스너를 선택합니다.
이벤트 채널에 헤더 속성을 사용하여 레이블을 지정한 경우 여러 이벤트를 역추적하여 로직의 흐름을 파악할 수 있습니다.

스크립터블 오브젝트 리소스 추가
이벤트 채널과 이벤트 중심 아키텍처가 여러분의 신규 및 향후 프로젝트에 도움이 되기를 바랍니다.
기술 전자책에서 스크립터블 오브젝트를 사용한 디자인 패턴에 대해 자세히 알아보세요, 스크립터블 오브젝트로 Unity에서 모듈식 게임 아키텍처 제작하기. 다음에서 일반적인 Unity 개발 디자인 패턴에 대해 자세히 알아볼 수도 있습니다. 게임 프로그래밍 패턴으로 코드 수준 높이기.