20240920 / Unity_6차 2주차 금요일
계속해서 C# 문법을 배우기 위해 혼자 묵묵히 수업을 듣는 나날...
강의를 계속해서 듣다가 Delegate(델리게이트, 대리자)에 대해 다루는 것을 보았다.
Delegate
Delegate란, 메서드를 참조하는 타입을 말한다.
참조를 포인터라 칭하는 C++에서는
'함수포인터' 라고 하는 Delegate와 동일한 기능이 있다.
Delegate를 사용하면 함수 그 자체를 변수처럼 전달하고 호출할 수 있다.
사용법은 아래와 같다.
'접근제어자' delegate '반환형' 'Delegate명'('매개변수')
delegate 만 빼면 함수 선언과 똑같다.
그러나 위와 같이 하면 'Delegate명'을
변수를 선언할 때 쓰는 '자료형'이나,
클래스 인스턴스를 선언할 때 쓰는 '클래스명' 처럼 쓸 수 있게 된다.
public delegate void DeleName();
요렇게 해두면
static void Main(string[] args)
{
DeleName Dele1, Dele2;
Dele1 = Func1;
Dele1();
}
static void Func1()
{
Console.WriteLine("Func1 호출");
}
이렇게 변수 선언하듯이 쓰고,
같은 자료형을 가진 변수를 대입하듯이 같은 반환형과 매개변수를 가진 함수를 대입할 수 있으며,
그렇게 선언한 변수(아님)는 사실 함수이기에 함수처럼 사용할 수도 있다.
※위의 예에서 'Dele2'는 초기화되지 않아 null값을 가지므로 쓸 수 없다
그냥 함수를 직접 호출하면 되는데 왜 이런 방법을 쓰냐, 하면 대표적으로
Callback과 Event를 위해 사용한다.
Callback은 외부 함수나 멀티스레드를 사용할 때 쓰는데 이건 지금 다루기 어려우니 넘어가고...
Event
Event는 기술적으로 Delegate에 제약조건을 추가하는 키워드지만
그보다는 클래스 내에서 일어난 일(event)를 외부로 알리고 그에 대한 응답을 받기 위한
알림이라는 기능적 측면으로 이해하면 편하다.
사실 Event 같은 경우는 WindowsForm을 만들어 봤다면 모를래야 모를 수가 없다.
그러나 게임 제작을 위해 배우고 있는 만큼,
게임을 예시로 들기 위해 Event를 활용하는 'Unit'이라는 클래스를 만들어 보았다.
public delegate void AggressiveEventHandler(Unit attacker, Unit victim);
public class Unit
{
string name;
public string Name
{
get { return name; }
}
int health;
public event AggressiveEventHandler OnHit;
public event AggressiveEventHandler OnDie;
public Unit(string _name, int _hp)
{
name = _name;
health = _hp;
}
public void HitBy(Unit attacker, int damage)
{
Console.WriteLine("{0}가 {1}로부터 {2}의 데미지를 입음!", Name, attacker.Name, damage);
health -= damage;
if (health <= 0)
{
Console.WriteLine("{0}가 {1}로 인해 체력이 소진됨!", Name, attacker.Name);
OnDie?.Invoke(attacker, this);
}
else
{
Console.WriteLine("{0}의 남은 체력 : {1}", Name, health);
OnHit?.Invoke(attacker, this);
}
}
}
앞으로 체력을 가지고 피격당할 수 있는 모든 객체는 이 Unit 클래스의 인스턴스로 선언할 것이다.
피격을 구현하기 위해 'HitBy'라는 함수를 만들고, 매개변수로 공격자 Unit과 데미지를 받는다.
공격받은 후에는 그에 대한 응답을 외부에서 함수의 형태로 짤 수 있도록
공격자 Unit과 피격자 Unit을 인수로 넘겨 주는 Delegate로 만들었다.
'HitBy' 함수 안에서 'AggressiveEventHandler'형 이벤트인 'OnHit'과 'OnDie'를 호출할 수 있도록 하면 준비는 끝났다.
※중간에 OnDie?.Invoke(attacker, this) 는 OnDie가 null이 아니면 이벤트 함수를 호출하겠다는 뜻이다
이제 'OnHit' 이벤트와 'OnDie' 이벤트를 활용할 수 있도록 유닛을 몇개 만들어 볼 것이다.
유닛은 player, enemy1, enemy2 이렇게 셋이 있다.
맞는 즉시 enemy1은 공격자에게 공격태세를 취하고, enemy2는 도주하려는 움직임을 보인다.
enemy2는 도망가다가도 죽을 때에는 공격자에게 달려들어 자폭한다.
player는 OnDie 이벤트가 호출되면 게임오버된다.
위의 조건을 만족하도록 프로그래밍 하면
static void Main(string[] args)
{
Unit player = new Unit("Hero", 50);
Unit enemy1 = new Unit("mob1", 20);
Unit enemy2 = new Unit("mob2", 15);
enemy1.OnHit += Attracted;
enemy2.OnHit += RunAway;
enemy2.OnDie += SuicideBombing;
player.OnDie += GameOver;
//플레이어 공격 턴
enemy1.HitBy(player, 7);
enemy2.HitBy(player, 7);
Console.WriteLine();
//적의 공격 턴
player.HitBy(enemy1, 5);
Console.WriteLine();
//플레이어 공격 턴
enemy2.HitBy(player, 9);
Console.WriteLine();
//적의 공격 턴
player.HitBy(enemy1, 5);
Console.WriteLine();
}
static void Attracted(Unit attacker, Unit victim)
{
Console.WriteLine("{0}가 {1}를 향해 공격 태세를 갖춤!", victim.Name, attacker.Name);
}
static void RunAway(Unit attacker, Unit victim)
{
Console.WriteLine("{0}가 {1}로부터 도주를 시도함!", victim.Name, attacker.Name);
}
static void SuicideBombing(Unit attacker, Unit victim)
{
Console.WriteLine("{0}가 {1}를 향해 자폭!", victim.Name, attacker.Name);
attacker.HitBy(victim, 40);
}
static void GameOver(Unit attacker, Unit victim)
{
Console.WriteLine("{0}는 눈앞이 캄캄해졌다!", victim.Name);
}
이벤트 발생 시의 동작을 함수로 만들어 놓고 각각의 이벤트에 += 연산으로 함수를 등록해 놓는다.
※event로 선언한 delegate에는 대입 연산자( = )를 사용할 수 없다
공수를 교대하면서 'HitBy' 함수를 호출한다.
프로그램을 실행시키면
요렇게 Event에 등록해놓은 함수들이 같이 실행된다.
사실 위의 'Unit' 클래스에서 event 키워드를 빼버려도 전혀 문제없이 동작하긴 한다.
그런데 이렇게 하면 클래스 밖 Main 함수에서도
player.OnDie()
이런 식으로 대리자를 직접 호출할 수 있게 된다.
(그렇다고 접근자를 private나 protected로 바꾸면 아예 밖에서 함수 추가를 할 수 없게 된다!)
그러면 "클래스 내에서 일어난 일을 외부에 알림" 이라는 기능으로는 쓸 수 없게 되어버린다.
그러므로 event 키워드를 꼭 쓰자.
event는 delegate와 같이 증가 연산을 중첩해 하나에 여러 함수를 포함시킬 수도 있다.
static void Main(string[] args)
{
Unit player = new Unit("Hero", 50);
Unit enemy1 = new Unit("mob1", 20);
enemy1.OnHit += Attracted;
enemy1.OnHit += ApplyThornmail;
enemy1.HitBy(player, 7);
Console.WriteLine();
}
static void Attracted(Unit attacker, Unit victim)
{
Console.WriteLine("{0}가 {1}를 향해 공격 태세를 갖춤!", victim.Name, attacker.Name);
}
static void ApplyThornmail(Unit attacker, Unit victim)
{
int reflectDamage = 2;
Console.WriteLine("{0}가 {1}에게 {2}의 데미지를 반사!", victim.Name, attacker.Name, reflectDamage);
attacker.HitBy(victim, 2);
}
반대로 기존에 등록된 함수를 해제하기 위해서는 -= '함수명' 을 하면 된다.
이로써 기존에 만들어 놓은 이벤트에 구현하고자 하는 동작을 붙였다 뗐다 할 수 있게 된 것이다!
번외.
static void Main(string[] args)
{
Unit player = new Unit("Hero", 50);
Unit enemy1 = new Unit("mob1", 20);
player.OnHit += ApplyThornmail;
enemy1.OnHit += ApplyThornmail;
enemy1.HitBy(player, 7);
Console.WriteLine();
}
static void ApplyThornmail(Unit attacker, Unit victim)
{
int reflectDamage = 2;
Console.WriteLine("{0}가 {1}에게 {2}의 데미지를 반사!", victim.Name, attacker.Name, reflectDamage);
attacker.HitBy(victim, 2);
}
무한반사!!!!!