Event Convention
C#의 이벤트 관련 컨벤션은 꽤나 복잡하다. 개발하는 조직에 따라서도 많이 달라질 수 있으므로 네이밍 컨벤션의 경우에는 현재 속한 조직에 맞게 적용하는것이 바람직할 것이다. 다만, 유니티에서 이벤트와 연관된 개념들이 어떻게 분리되어야 하는지, 어떤 그룹으로 구분되어야 하는지에 대해서는 정리해둘 필요가 있다. 구분 기준을 명확하게 정해두었다면 조직에 따라 네이밍 기준이 달라지더라도 개발에 있어서 컨벤션을 통한 생산성을 극대화시킬 수 있을 것이다.
다음은 MS에서 제공하는 이벤트 네이밍 컨벤션이다.
https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members#names-of-events
다음은 유니티에서 제공하는 이벤트 네이밍 컨벤션이다.
https://unity.com/how-to/naming-and-code-style-tips-c-scripting-unity#events-and-event-handlers
두 문서를 모두 참고하고 개인 경험을 조합하여 최선의 이벤트 컨벤션을 정의한다.
Event
1. 이벤트는 동사나 동사구의 형태로 네이밍한다.
Clicked, Painting, DroppedDown
2. 이벤트에서는 작업 이전과, 작업 이후에 대한 정보가 네이밍에 반영되어야 한다. 이 부분을 동사와 시제를 이용하여 표현하도록 한다. 꽤 많은 개발자들이 Before, After와 같은 prefix를 이용하여 이 부분을 표현하는데 MS에서는 시제를 활용할것을 권한다. 그러므로 Before, After, Pre, Post와 같은 prefix는 피하도록 한다.
// x
public event EventHandler PrevItemSelect;
// x
public event EventHandler PostItemSelect;
// o
public event EventHandler ItemSelecting;
// o
public event EventHandler ItemSelected;
3. 유니티에서는 이벤트를 정의하기 위한 delegate을 매번 만들지 말고 Action과 Func를 사용하는것을 권한다. (MS와는 조금 다르다. 순수 C# 개발이라면 MS 컨벤션을 따르겠지만 유니티 C#에서는 유니티 컨벤션을 우선으로 한다.)
// using System.Action delegate
public event Action OpeningDoor; // event before
public event Action DoorOpened; // event after
public event Func<int, bool> onValidatePressOkBtn; // event with return value
4. 이벤트 변수에는 "on" prefix를 이용하지 않는다. MS에서 제공하는 컨벤션 문서를 참고하면 "on" prefix는 이벤트를 발생시키기 위한 용도로 사용되기 때문에 이벤트 변수 자체에는 쓰지 않기를 권하고 있다.
// x
public event Action<ICard, EnumTypes.OriginStageTypes> onUpgradePreviewBtnPressed;
public void OnUpgradePreviewBtnPressed(ICard card, EnumTypes.OriginStageTypes originStageType)
{
upgradePreviewBtnPressed?.Invoke(card, originStageType);
}
// o
public event Action<ICard, EnumTypes.OriginStageTypes> upgradePreviewBtnPressed;
public void OnUpgradePreviewBtnPressed(ICard card, EnumTypes.OriginStageTypes originStageType)
{
upgradePreviewBtnPressed?.Invoke(card, originStageType);
}
5. 이벤트 핸들러의 경우 "EventHandler" suffix로 네이밍한다.
다만, 이는 MS의 표준이고 Unity에서는 대부분의 EventHandler로 Action과 Func가 사용되기 때문에 이 컨벤션이 적용되는 경우는 많지 않을 것이다.
public delegate void ClickedEventHandler(object sender, ClickedEventArgs e);
6. 이벤트 파라메터로 object sender와 e를 넘겨주도록 한다. 여기서 sender는 이벤트를 발생시킨 주체를 의미하고, e는 이벤트 발생 시 넘겨질 인자들을 의미한다. 다만, 유니티에서는 Action과 Func가 가변인자를 지원하기 때문에 인자가 많아지는 경우에 이벤트 파라메터를 따로 정의하여 e로 넘겨주는것이 좋을 것이다.
public event Action<object, System.EventArgs> upgradePreviewBtnPressed;
public void OnUpgradePreviewBtnPressed(object sender, System.EventArgs args)
{
upgradePreviewBtnPressed?.Invoke(sender, args);
}
이 부분에 있어서 드는 의문이 있다. 만약 sender나 args를 강타입으로 넘겨준다면 어떨까? 즉 이벤트를 발생시킨 객체의 타입을 그대로 사용하는것이다.
object sender -> MyType sender
이렇게 한다면 핸들링 함수에서 굳이 캐스팅을 할 필요도 없고 더 직관적으로 이벤트를 처리할 수 있을 것이다. 이에 대해서는 일반화와 특수화의 장단점을 고려하여 현재 프로젝트에서 어떤 방향이 적합할지 우선순위를 매기는 것이 좋을 것이다. 다음은 이 주제에 대한 stackoverflow의 토론 내용이다. stackoverflow에서는 더 많은 확장성을 위해서 object sender, EventArgs args 를 이용하는 것이 좋을 것이라는 의견이 우세하다. (다만, 일반화의 장점을 얻으려면 sender만 object로 통일해서는 안되고 args도 EventArgs로 통일해야 가능할 것 같다.)
7. 이벤트를 핸들링하는 처리 함수에서는 이벤트를 발생시키는 주체의 이름과 이벤트 이름을 underscore로 연결하여 네이밍하도록 한다. 예를 들어 발생시키는 주체가 "GameEvents"이고 이벤트 이름이 "DoorOpened"이면 처리 함수는 GameEvents_DoorOpened가 될 것이다.
Global Event
인스턴스에 종속적이지 않고 어느 곳에서나 참조 가능한 이벤트이다. 이벤트를 이용할 때 어려운 부분 중 하나는 이벤트를 제공하는 객체의 인스턴스를 이벤트를 구독하려는 객체에서 참조해와야 한다는 것이다. 물론 이렇게 구현하는 것이 안정적이지만, 반드시 인스턴스에 종속적일 필요가 없는 이벤트들은 글로벌 이벤트의 형태로 어디서나 참조 가능하도록 만들 수 있다. 이렇게 하면 편의성을 극대화시킬 수 있지만 너무 많이 남용되면 참조관계가 복잡해질 수 있으니 적절히 잘 활용하는 것이 좋다. 글로벌 이벤트는 보통 static event나 signal과 같은 형태로 구현할 수 있다.
Static 구현
글로벌 이벤트의 가장 기본적인 구현방법이다. 이렇게 static으로 구현된 이벤트는 어디에서나 구독/취소 가능하고, 이벤트가 발생되면 모든 구독된 객체들에게 이벤트가 전달된다. 다만 이 구현의 단점은 구독 후 제거를 반드시 신경써서 해줘야한다는 것이다. static으로 정의된 이벤트에 파괴된 객체의 이벤트 핸들러가 남아있게된다면 아주 번거로워질 수 있다.
// define
public static class Events
{
public static event Action<int> btnPressed;
public static void OnBtnPressed(int index)
{
btnPressed?.Invoke(index);
}
}
// raise
Events.OnBtnPressed(1);
// add, remove
Events.btnPressed += Handle_BtnPressed;
Events.btnPressed -= Handle_BtnPressed;
Signal 구현
Notification Manager를 통해 글로벌 이벤트를 구현할 수도 있다. Signal은 C#으로 개발된 가벼운 형태의 Notification Manager인데, 이 형태로 글로벌 이벤트를 구현할 경우 매번 raise 함수를 만들어줄 필요가 없어지고, 이벤트의 형태를 좀 더 직관적으로 정의할 수 있다. 또한 Signals 객체를 통해 일괄적으로 이벤트들을 제거하는것도 가능하기 때문에 static event를 이용하는 방법보다 좀 더 안정적으로 이벤트를 관리할 수 있다. 다만 이벤트를 호출할 때마다 해싱 비용이 발생하게 된다.
// define
public static class Events
{
public class BtnPressed : ASignal<int> { }
}
// raise
Signals.Get<Events.BtnPressed>().Dispatch(1);
// add, remove
Signals.Get<Events.BtnPressed>().AddListener(Handle_BtnPressed);
Signals.Get<Events.BtnPressed>().RemoveListener(Handle_BtnPressed);
다음은 오픈소스 Signals 레포지토리 링크이다.
https://github.com/yankooliveira/signals
Local Event
인스턴스에 종속적인 이벤트이다. 보통 로컬 멤버 변수로 구현된다.
로컬 이벤트 정의는 다음 규칙들을 따른다.
1. 이벤트 멤버변수에는 underscore를 넣어 멤버변수임을 표시하고, private으로 감춘다.
private event Action _attackHit;
2. 이벤트를 외부로 노출시키기 위해 프로퍼티를 이용한다. 프로퍼티는 pascal case로 네이밍하고 위 이벤트 네이밍 규칙에 따라 동사의 형태로 네이밍한다. add, remove를 구현한다. 변수를 감추고 프로퍼티만 노출시킴으로써 변수에 null이 세팅되는 상황을 방지할 수 있고, 참조에 대한 추적을 좀 더 원할하게 할 수 있다.
public event Action AttackHit
{
add => _attackHit += value;
remove => _attackHit -= value;
}
3. 이벤트를 발생시키는 raise 함수는 "on" prefix를 이용하여 구현하고, 필요한 경우 가상함수로 만들어 서브객체에서 재활용할 수 있도록 한다.
public virtual void OnAttackHit()
{
_attackHit?.Invoke();
}
Handling Function
이벤트 핸들링 함수들은 이벤트를 발생시키는 객체가 아니라 이벤트를 구독하는 객체에서 정의되는 함수들을 의미한다. 여기에서는 이벤트 핸들링 함수들을 어떤 형태로 관리하고, 어떻게 구분하는지에 대해 정리한다.
1. 핸들링 함수는 로컬 이벤트에 구독하는 경우 "인스턴스 이름" + "_" + "이벤트 이름" 의 형태로 함수 이름을 네이밍한다. 예를들어 _animListener라는 멤버변수의 AttackHit이라는 이벤트를 구독하는 경우 핸들링 함수는 다음과 같을 것이다.
_animListener.AttackHit += AnimListener_AttackHit;
2. 핸들링 함수는 초기화될 때 구독되어야 하고 파괴될 때 구독이 제거되어야한다.
// call this at init
_animListener.AttackHit += AnimListener_AttackHit;
// call this at destroy
_animListener.AttackHit -= AnimListener_AttackHit;
3. UI prefab을 통해 연결된 이벤트는 따로 분류하도록 한다. 유니티나 언리얼이나 리펙토링 할 때 가장 건드리기 어려운 코드가 외부 리소스와 연결된 함수들이다. 이들은 코드에서와 달리 컴파일러의 도움을 받을 수 없기 때문에 어떤 리소스를 통해 참조되는지 추적하는것이 어렵다. 그러므로 프리팹과 연결되는 함수들은 따로 분류해두는 것이 효과적이다. 네이밍도 기존과 달리 "UIEvent" prefix를 이용한다.
#region UI Event Handlers
public void UIEvent_PressBackBtn()
{
// ..
}
public void UIEvent_PressRightBtn()
{
// ..
}
#endregion UI Event Handlers
코드 내 객체를 통한 이벤트
Unity Engine을 통한 이벤트 (OnDestroy, OnEnable ..)
다음은 완성된 예제이다.
using System;
public class AnimationEventListener : BaseMonoBehaviour
{
private event Action _attackHit;
public event Action AttackHit
{
add => _attackHit += value;
remove => _attackHit -= value;
}
// anim clip event handler
public void AnimClip_AttackHit()
{
// relay to raise
OnAttackHit();
}
// raise function
private void OnAttackHit()
{
_attackBegin?.Invoke();
}
}
public class CharacterObject
{
protected AnimationEventListener _animListener;
// ..
private void AddEventHandlers()
{
_animListener.AttackHit += AnimListener_AttackHit;
}
private void RemoveEventHandlers()
{
_animListener.AttackHit -= AnimListener_AttackHit;
}
private void AnimListener_AttackHit()
{
// 이벤트 핸들링 함수에서 다시 글로벌 이벤트를 발생시킴
Events.OnAnimListener_AttackHit(this);
}
public static class Events
{
private static event Action<CharacterObject> _animListener_AttackHit;
public static event Action<CharacterObject> AnimListener_AttackHit
{
add { _animListener_AttackHit += value; }
remove { _animListener_AttackHit -= value; }
}
public static void OnAnimListener_AttackHit(CharacterObject characterObject)
{
_animListener_AttackHit?.Invoke(characterObject);
}
}
}
'게임 엔진 > Unity' 카테고리의 다른 글
[Unity] 유니티 설계 경험 기록 (22) | 2021.11.12 |
---|---|
[Google Play] 구글 플레이 결제 시스템(IAP) 세팅 (0) | 2021.08.08 |
[Google Play] 구글 플레이 게임 서비스 세팅 (2) | 2021.08.07 |
[Unity] [Example] 유니티에서 데이터 관리 방법(ScriptableObject) (0) | 2021.07.12 |
[Unity] [Example] 평면 연산 방법 (Plane) (0) | 2020.07.28 |