[Unity] Events와 Delegate에 대해

jh Seo·2023년 12월 29일
0

유니티

목록 보기
38/42

개요

public delegate void OnFound();
public static event OnFound onFound;

이런 형식의 event나

public UnityEvent onFound;  

이런 유니티 이벤트 형식을 자주 봤지만 익숙하지 않아서 정리하는 글이다.

https://gamedevbeginner.com/events-and-delegates-in-unity/

이 분의 포스팅을 읽고 요약,정리해보는 글이다.

Observer Pattern

이벤트가 발생했을 때 각 게임 오브젝트에게 이벤트 발생여부를 알려주고 실행시키는 옵저버 패턴은
로직들을 모듈화시킬 수 있다.

또한, update문이나 코루틴을 통해 계속 상태를 체크하는 비효율적인 방식보다는
옵저버 패턴을 이용하면 이벤트 발생을 통해 바로 함수실행이 가능해 더 효율적이다.

유니티에서 사용하는 대표적인 옵저버 패턴은 delegate이다.
c에서 사용하는 함수포인터와 비슷하다.

Delegate

parameter없는 delegate

이런식으로 delegate를 선언해준 후(MyDelegate), delegate 변수(func)를 선언해준다.
이제 func는 void반환형, void parameter을 가진 함수면 뭐든 할당할 수 있다.

이런 식으로 할당 후, 실행해보면

잘 소리지른다.

paramter있는 delegate

parameter과 반환형을 변경할 수 있다.
parameter만 변경한 예시)

이런 식으로 delegate을 호출할 때 인자값을 넣어줘야 한다.
func? 이런식으로 delegate에 함수가 할당되어 있는지 확인할 수 있다.

사용 예

이런 식으로 공격을 할때마다 delegate에 붙였다 떼었다 할 수 있다.

다양한 함수 attach 가능

또 다른 좋은 점으로는 delegate에 다양한 함수를 attach 할 수가 있다!!
+=연산자를 이용하는 것이다.

실행시켜보면

냉탕 온탕 둘다 느낄 수 있었다.
정리하다 delegate에 +=을 다해놓고 마지막에 =을 해버리면 다른 변수들처럼
더해진 게 사라지느지 궁금해졌다.


다른 변수들과 동일하게 마지막에 할당해버리면 다 사라졌다!

외부 코드들도 delegate에 attach가능

바로 public과 static을 이용해 데이터 영역에 올려놓음으로써 외부에서도 접근 가능하게 만드는 것이다.

위와 같이 delegate변수를 static으로 선언해주면 된다.
연습을 위해 잔인하지만 마우스클릭 한번으로 게임오버를 시키도록 했다.

위 스크립트는 onGameOver가 선언된 스크립트와 다른 스크립트이다.
static으로 선언하면 해당 스크립트를 getcomponent함수를 사용해 불러오지 않고,
DelegateGameOver.onGameOver이런식으로 바로 호출이 가능하다.

마우스 클릭을 해보면 attach된 함수들이 곧잘 실행된다.

주의할 점은 OnDisable을 이용해 함수를 제거해야한다.
아니면 onEnable이 실행될 때마다 계속 delegate에 attach되어서 동일한 함수가 계속 실행된다!

위 방식의 문제점

하지만 delegate에도 문제점이 있으니 public static으로 선언해버려서
모든 함수가 해당 delegate를 접근가능과 동시에 실행시킬 수 있다.
실수로 다른 코드에서 onGameOver을 접근해서 실행해버리면
그대로 게임이 끝나게된다.

Events

delegate와 동일하게 작동한다.
차이점은 뭐냐? 바로 Event를 사용하면 함수 실행이 해당 스크립트 내에서만 가능하다는 점이다.
위 예시에서 event키워드를 붙인 후, 외부코드에서 실행해보자.

onGameOver 이벤트는 +=또는 -=의 왼쪽에만 사용할 수 있습니다. 라고 오류가 발생한다.

단, DelegateGameOver형식에서 사용될 때는 예외인데 DelegateGameOver형식이
onGameOver이벤트가 선언된 스크립트다.

Event와 Delegate

하지만 Event가 꼭 delegate보다 좋다라고 할 수는 없다.

deleagate는 그저 함수 포인터로 사용도 가능해서 parameter에 delegate를 넣기도 한다.
이때 다른 함수에서 이 함수를 호출하는 식으로도 가능하다.

위는 간단한 예제로
delegate를 이용해 parameter로 함수를 넘길수가 있다.

설계에 따라 유의하여 둘 다 사용하면 좋을 것 같다.

Actions

Actions는 ready-made delegate 즉 미리 만들어진 delegate이다.

using System;

를 추가해야 사용이 가능하다.

Action은 반환형이 void이다.

주석 처리된 부분과 onGameOver Action은 동일하게 작동한다.
실행시켜보면

위 결과와 똑같이 작동한다.

Action은 반환형은 없지만 paramter을 넘길 수 있다.

public static event Action<int,int, float,string> action;

parameter은 위 코드처럼 <>를 통해 표시해준다.

action?.Invoke(3, 4, 0.5f, "action 기가막힌다~");

action에 위 actionPrac함수를 할당 후, 실행해주면

잘 실행이 되는걸 확인할 수 있다.

Unity Events

unity events는 delegate 와 event와 동일하게 작동한다.
차이점은 serialize가 되어 unity의 inspector창에서 확인할 수 있다!

자주 사용하는 UI인 Button 컴퍼넌트의 OnClick()또 unityevent라고 한다.

unity event를 사용하기 위해선

using UnityEngine.Events;

를 추가해야한다.

public delegate void GameOver();
public event GameOver onGameOver;

위 코드가 이제 unity event를 사용하게되면

public UnityEvent onGameOver;

이렇게 바꿀 수 있다.

바꾸게 되면 inspector창이

이런식으로 추가되고, button UI에서 하듯이 +누르고 오브젝트를 drag and drop을 통해 만들 수 있다.

unityevent는 기본적으로 null 체크가 알아서 되기 때문에
? 연산자를 사용 안하거나 null과의 비교를 안해도 에러가 발생하진 않는다.

script내에서 unityevent에 함수를 추가할때는 delegate처럼 += 연산자를 사용할 수 없다.
대신 AddListener함수나 RemoveListner함수를 사용해 추가 및 제거를 해야한다.

 private void OnEnable()
{
	delegateGameOver.onGameOver.AddListener(DenyGameOver);
	delegateGameOver.onGameOver.AddListener(AcceptGameOver);
}
private void OnDisable()
{
	delegateGameOver.onGameOver.RemoveListener(DenyGameOver);
    delegateGameOver.onGameOver.RemoveListener(AcceptGameOver);
}

unity events에 parameter을 넣는 법

이런 식으로 클래스를 serializable시킨 후, UnityEvent를 상속받는식으로 구현해야한다.
FloatEvent는 float변수를 parameter로 받고, StringEvent는 string형 변수를 parameter로 받는다.

public FloatEvent onDamaged;
public StringEvent echo;

이런 식으로 선언하여 사용할 수 있다.
동일하게 AddListener을 이용해 attach를 한다.

onDamaged.Invoke(3.5f);
echo.Invoke("si~");

event에 함수들을 attach후 위 코드를 실행 해보면

잘 실행이 된다!.

Scriptable Objects Unity Events

좀 더 함수들을 modular하게 만드는 방법이다.
scriptable object를 사용해 event를 관리하는 방식이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Game Event")]
public class GameEvent : ScriptableObject
{
    private List<GameEventListener> listeners = new List<GameEventListener>();
    public void TriggerEvent()
    {
        for(int i=listeners.Count-1; i >= 0; i--)
        {
            listeners[i].OnEventTriggered();
        }
    }
    public void AddListener(GameEventListener listener)
    {
        listeners.Add(listener);
    }
    public void RemoveListener(GameEventListener listener)
    {
        listeners.Remove(listener);
    }
}

GameEvent라는 클래스를 Scriptable object로 선언해준다.
Menu 이름은 Game Event로 해줬다.
이렇게 선언시, GameEvent가 subject가 되어서 나머지 observer들을 관리한다.

다음은 observer클래스인 GameEventListener클래스이다.

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

public class GameEventListener : MonoBehaviour
{
    public GameEvent gameEvent;
    public UnityEvent onEventTriggered;
    private void OnEnable()
    {
        gameEvent.AddListener(this);
    }
    private void OnDisable()
    {
        gameEvent.RemoveListener(this);
    }
    public void OnEventTriggered()
    {
        onEventTriggered.Invoke();
    }
}

GameEventListener클래스는 MonoBehaviour를 상속받아 컴퍼넌트로 붙일 수 있도록 한다.

작동 방식은 subject오브젝트의 컴포넌트에 GameEvent를 멤버변수로 넣고
할당해준다.

observer오브젝트에는 GameEventListener클래스를 컴퍼넌트로 attach해준다.

구조는 위와 같은 방식이고, 실행 시킬때는 subject클래스에서 원하는 시점에

onGameOver.TriggerEvent();

TriggerEvent함수를 호출해주면 된다.

위 방식을 활용하게 되면 클래스들이 서로 묶여있지 않아서 한결 자유롭게 구현할 수 있을 것같다.

레퍼런스

https://gamedevbeginner.com/events-and-delegates-in-unity/

profile
코딩 창고!

0개의 댓글