유니티에서 델리게이트 delegate는 참 많이 쓰인다. 정확한 정의는 뭔지는 몰라도 대충 함수를 대행? 콜백함수? 이다라는 것은 아니까 나는 항상 사용할 때 Action변수를 만들어서 프로퍼티 Setter와 묶어서 많이 사용했던 것으로 기억한다. 그런데 여기서 문제가 있다. 저번에 면접 보러간 회사에서 delegate의 정의가 무엇인지 설명해 보라고 했다. 대답을 못했다.. 대애애충 ~~ 델리게이트 액션 같이 묶어서 쓰고 뭐~~ 콜백적인 기능이고 뭐고~~~ 이런식으로만 말을 했다... 하지만 그렇게 말하면서도 아 떨어지겠구나.. 라고 생각했다. 그 뒤로 델리게이트의 정의를 다시 공부했다. 하지만 또 까먹었따!! 그래서 써본다 델리게이트란 무엇인가?? 오늘 파해쳐보자!!
1. 유니티 이벤트
먼저 델리게이트를 설명하기 전에 유니티 이벤트(Unity Event)에 대해서 설명해보자. 이벤트는 기본적으로 이벤트가 발동이 되면 즉, 사건이 발동이 되면 그 사건에 등록을 해놓은 기능들이 자동적으로 딸려 들어가서 같이 발동이 되는 거다. 그런데 이벤트의 특징이 뭐냐면 이벤트를 발동시키는 측도, 그 이벤트에 기능을 등록해놨던 측도 서로에게 관심이 없는 거란 얘기다. 즉 이벤트 기능을 등록해 놓은 오브젝트들은 그냥 거기에다가 기능을 등록해놓고 그게 언제 발동될지 혹은 어떻게 발동될지를 신경을 전혀 쓰지 않는다. 이벤트가 그럼으로써 코드가 서로 스파게티처럼 엮이지 않도록 해주는 강력한 장점이 있다!!
public class PlayerHealth : MonoBehaviour
{
public UnityEvent onPlayerDead;
private void Dead()
{
onPlayerDead.Invoke();
Destroy(gameObject):
}
}
플레이어가 사망했을 때 해야할 일들에 대해서 생각해보자. 플레이거가 사망하면 ui에는 플레이어 사망이라는 텍스트가 떠야 할 것이고, 게임 씬이 종료되어야 할 것이고, 도전과제가 실패했다고 처리도 해 줘야 할 것이고 등등 코드로 처리해야 할 일들이 너무 많다. 이런 것들을 처리하려면 만약 이벤트 시스템을 몰랐다면 PlayerHealth라는 스크립트의 변수에 UIManager, AchieveManager, GameSceneSystem 등 Player라는 게임 내적인 부분과는 관련없는 게임의 외적인 시스템 부분을 Player가 알아야 할 것이다. 이것이 이치에 맞다고 생각하는가? 그렇지 않다. 게임 내 요소와 외적 요소를 구분해 코드를 짜야지 깔끔하고 이치에 맞고 스파게티같은 코드를 막을 수 있다. 그렇다면 위에 이런 처리들을 어떻게 처리해야 하는가? UnityEvent 변수 하나를 만들고 단순히 Invoke() 해주면 된다. 이게 끝이다. 그리고 플레이어의 죽음에 관심있어 하는 것들을 onPlayerDead 변수에 등록시켜 놓는다.
2. delegate
unity event는 우리에게 쉽게 사용할 수 있도록 유니티에서 제공하는 오브젝트이고, 그 밑에는 c#에서 제공하는 이벤트와 델리게이트가 있다. 그래서 그게 무엇이고 어떻게 사용하는지를 설명해보자.
그래서 delegate를 먼저 설명을 해보자면 delegate는 영어로 '대행하다'라는 뜻이다. 무슨 말이냐면 delegate는 어떠한 기능을 거기 목록에 추가해 놓으면 이 delegate가 우리의 기능들을 대신 발동시켜준다.
그래서 c#에서는 delegate라는 친구가 미리 제공되는데 기본적으로 delegate는 유언대리인데 많이 비유를 한다. 무슨 말이냐면 유언대리인은 유언장을 대신 수행해 준 친구인데, 즉 유언대리인이 있다. 그런데 이 사람은 유언장에 적힌 내용물을 대행해 준다. 곧 죽을 사람이 이 리스트에다가 자기가 하고 싶은 내용물을 명시를 한다. 그 다음 이 사람은 그냥 저세상으로 가버린다. 그러면 이 유언대리인은 유언리스트에 어떤 친구들이 있는지 전혀 신경쓰지 않는다. 단순히 유언리스트에 있는 내용물들을 어떤 내용인지 모르겠지만 그냥 수행을 해버린다. 그게 델리게이트이다.
델리게이트는 어떠한 기능들을 리스트에다가 등록을 하고 그 등록된 기능들을 쭉 대응해주는 친구이다. 단 거기에는 서식이 있어서 맞는 서식을 갖지 않는 함수는 필터링을 하는 기능이 들어있다. 그래서 다시 delegate를 봤을 때 delegate는 우리들이 delegate 형을 먼저 선언을 한다. 무슨 말이냐면 delegate형은 이런 형태의 함수만 대신 해주겠다는 명시라고 보면 된다. 그래서 delegate는 일종의 명부 혹은 명단이라고 보면 되는데 여기 명단에가다 함수들을 이렇게 등록을 한다.
그런데 사실은 delegate는 내부에 포인터라는걸 가지고 있다. 정확히 얘기하면 함수 포인터라는걸 가지고 있는데 a라는 기능, b라는 기능, c라는 기능 정확히 얘기하면 함수죠. a,b,c라는 함수들이 여기다가 자신을 등록한다는 얘기는 무슨말이냐면 사실 delegate가 이 a, b, c를 가리키는 변수를 가지고 있다는 얘기이다. 즉 delegate는 자신의 유언장에다가 a,b,c라는 명단을 기록을 해놓고 그리고 delegate가 발동되는 순간 이 화살표들을 찾아가서 즉, 그 기능이 있는 곳까지 찾아가서 a,b,c를 발동시키는 것이다. 즉 b라는 기능을 가지고 있는 위치까지 쫓아가서 즉 포인터, 화살표라는 얘기이다. 정확히 얘깋면 이 delegate는 내가 대신 실행시킬 친구들의 집주소가 적혀 있다.
public class Calculator : MonoBehaviour
{
delegate float Calculate(float a, float b);
public float Sum(float a, float b)
{
return a + b;
}
}
코드로 확인해보자. 새로운 delegate 형을 만들어 줄 것이다. 그래서 변수를 만드는 것이 아니라 delegate 형을 새로 하나 정의하는 것이다. 무슨 말이냐면 delegate를 사용할 때 delegate가 받아들일 수 있는, 대행할 수 있는 함수의 서식을 결정해야 하는데 내가 만든 타입의 함수만 대신해 주겠다는 뜻이다.
그래서 delegate float 이거를 Calculate라고 하겠다. 자 이거는 새로운 함수를 만든게 아니라 혹은 새로운 변수를 만든게 아니라 새로운 delegate 형을 정의한 것이다. 즉 이 친구 자체는 object가 아니라 이 친구는 찍어낼 수 있는 delegate의 원본을 만든 것이다. 무슨 말이냐면 이 calculate 형의 delegate는 float이라는 숫자를 return하고 float이라는 숫자 2개를 입력으로 받는 함수만 대행해 줄 수 있다는 얘기이다.
public class Calculator : MonoBehaviour
{
delegate float Calculate(float a, float b);
Calculate onCalculate;
public float Sum(float a, float b)
{
return a + b;
}
}
그리고 타입에 새로운 delegate의 onCalculate라는 변수를 만들어 줄 것이다. delegate 변수다. 그래서 delegate float Calculate(float a, float b) 이 것은 타입이고, 타입의 delegate object를 실제로 만들었다. 그래서 이 onCalculate라는 친구가 대행해 줄 수 있다.
public class Calculator : MonoBehaviour
{
delegate float Calculate(float a, float b);
Calculate onCalculate;
void Start()
{
onCalculate = Sum;
}
public float Sum(float a, float b)
{
return a + b;
}
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
onCalculate(1,10);
}
}
즉, 지금 onCalculate(1,10)은 이 안에서 사실 Sum(1,10)이 발동이 되는거다. 단 우리의 Calculate는 발동될 때 어떤 친구를 발동할지를 굳이 알 필요가 없다는 것이 핵심이다. 즉, onCalculate가 Sum을 대리해 준 것이다.
onCalculate = Sum;
onCalculate = onCalculate + Subtract;
그리고 당연하게도 대리해주는 친구는 한 가지만 대리할 수 있는 것이 아니라 여러 가지를 심부름 할 수 있다.
public class Character : MonoBehaviour
{
public delegate void Boost(Character target);
public event Boost playerBoost;
}
public class Booster : MonoBehaviour
{
void Awake()
{
Character player = FindObjectofType<Character>()
player.playerBoost += HealthBoost;
player.playerBoost += ShieldBoost;
player.playerBoost = DamageBoost;
}
}
그렇다면 delegate로 이벤트를 만들 수 있다면 왜 c#에서 event라는 키워드가 있는것일까? delegate 앞에다가 event라는 키워드를 붙일 수 있는데 이 event라는 키워드는 정확하게 얘기하면 delegate가 이벤트가 아닌 방향으로 잘못 사용되지 않도록 제한해주는 역할을 한다.
delegate 자체는 일단 퍼블릭 변수인데 player.playerBoost에 '+=' 가 아니라 '='을 그냥 해버리면 덮어쓰기가 된다. 즉, playerBoost에 여태까지 구독해놨던 모든 기능들이 다 날아가 버리고 데미지 부스트만 남을 수가 있다. 이 상태에서 게임을 해 버리면 공격력을 강화했다라는 데미지 부스트만 적용이 된다. 즉 이벤트랑 구독자들이 여리게 자유롭게 등록을 하고 뺄 수가 있어야 하는데 누군가가 덮어쓰기 쉬워서 거기 구독된 친구들이 한꺼번에 날아가버리고 만다. 그런데 event라는 키워드를 앞에 붙이면 스크립트 부분에서 player.playerBoost = DamageBoost; 라는 덮어쓰는 곳에서 에러가 난다. 즉 냅다 덮어 쓸 수가 없다.
즉, 이벤트 키워드를 붙이면 delegate의 기능이 이벤트가 아닌 방향으로 작성되는 것을 막아준다. 실수를 막아주는 기능이라고 보면 된다. 그럼 이런식으로 생각할 수도 있다. 그럼 이벤트 키워드가 필요가 없네요? 네, 이벤트란 키워드가 없어도 코드를 주의깊에 짠다면 충분히 구현 가능하다. 문제는 코드 실수이다.
레트로의 유니티 C# 게임 프로그래밍 에센스 | 이제민 - 인프런
이제민 | 모든 분야의 초심자를 대상으로, 유니티 C# 게임 프로그래밍을 직관적으로 설명합니다., 2019년 새로운 추가 콘텐츠(TPS 게임 제작)가 연재 완료 되었습니다. retr0의 유니티 게임 프로그래
www.inflearn.com
'게임개발 > 스터디' 카테고리의 다른 글
[스터디] 람다 함수(익명 함수)란? (0) | 2024.04.16 |
---|---|
[스터디] 액션 Action 함수란? (0) | 2024.04.15 |
[스터디] Enum값을 flag로 사용하기 (0) | 2024.04.12 |
[스터디] FSM 패턴과 사용해야 하는 이유 (0) | 2024.04.11 |
[스터디] MonoBehavior 붙이고 안 붙이고 차이 (0) | 2024.04.11 |