프로그래밍(C#) /유니티공부

[유니티C# 스크립팅 마스터하기] - 4챕터/이벤트 주도적 프로그래밍

정체불명의 모모 2022. 5. 23. 15:51

이번장에서 배울 내용은 이벤트 주도적 프로그래밍입니다.

게임과 애플리케이션의 이벤트에 대해 알아보면서

적지 않은 성능 향상을 얻어 보도록 하겠습니다!!

그리고 이벤트에 대해 살펴 보고 

이벤트를 게임에서 어떻게 다루는지 알아 봅시다!

go! go!


이벤트

:  행동들 간에 중요한 관계를 맺고 있고, 이런한 연결이나 결합을 이벤트라고 부르는데, 각각의 개별적인 연결은 하나의 이벤트가 됩니다.

 

◻︎ 이벤트 주도적 프로그래밍

: 이벤트 주도적 프로그래밍은 게임 속 대부분의 사건들이 이벤트의 예라는 것을 알고서 이벤트를 일반적인 개념으로 이해하는 것으로 부터

   시작 됩니다.

   즉, 이벤트의 개념은 정해진 시간에 일어나는 것 뿐만 아니라 특정 시간에 발생하는 특정 이벤트를 포함 하는 것 입니다.

   

► Update 와 이벤트

: 게임 월드는 이벤트와 응답으로 조직된 온전한 시스템이라고 할 수 있습니다.

  이런 식의 월드에서 Update 함수가 매 프레임 동작을 진행하는 방식에 의존 하는 대신 이벤트를 사용하는 것이 성능을 향상

  시키는 데에 어떤 도움을 주는 지 의문이 생깁니다. 

 

▷ 해답

: 이벤트의 잦은 호출을 줄이는 것 입니다. 


✱ 아래의 예제는 
1. 체력이 떨어졌을 때 구급 상자를 찾아 체력을 회복할 수 있도록 적의 체력을 추적 
2. 탄약이 바닥 났을 때는 적으로 하여금 탄약을 더 찾아 모으고 언제 시야에 들어온 플레이어에게 발사할지 합리적으로
    판단할 수 있도록 탄약의 양을 추적

 

✣ Update 함수 이용

void Update()
{	// 적의 체력을 확인한다. 죽었는가
	if(Health <= 0)
    {
    	// 죽었다면 죽는 동작을 수행한다.
        Die();
        return;
     }
     
     // 체력이 낮은 상태인지 확인한다.
     if(health <= 20)
     {
     	//체력이 낮은 상태이면 구급상자를 찾는다.
        RunAndFindHealthRestoure();
        return;
     }
     
     // 탄약이 떨어졌는가 확인
     if(Ammo <= 0)
     {
     	// 탄약을 좀 더 찾아본다.
        SearchMore();
        return;
      }
      
      // 체력과 탄약이 충분하다. 플레이어가 보이면 발사하자
      if(HaveLineOfSight)
      {
      	FireAtPlayer();
      }
 }

위 예제 코드는 많은 조건 검사들로 가득 찬 무거운 Update 함수를 보여 줍니다.

본질적으로 Update 함수는 이벤트 처리 및 응답을 하나로 합치도록 이끌기 때문에 불필요하게 비싼 처리 과정을 만들어낸다.

이런 서로 다른 처리 사이의 이벤트 연결 관계에 대해 고민해 보면 어떻게 좀 더 코드를 깔끔하게 리팩토링 할지 알 수 있다.

 

아래의 코드는 위 코드를 c# 이벤트와 프로퍼티를 이용해서 리팩토링 하여 훨씬 작아진 Update 함수를 볼 수 있습니다.


✣ C# 프로퍼티를 이용한 리팩토링

이벤트 주도적인 설계를 함으로써 성능 최적화 및 청결한 코드 유지가 가능해 집니다.


이벤트 관리 ( EventListener / EventPoster / EventManager )

: 이벤트 주도적 프로그래밍을 이용하면 작업이 훨씬 용이해지는 면이 있습니다.

  이벤트가 발생했을 때 모든 오브젝트에게 모든 종류의 이벤트에 대해 선택적으로 수신 하도록 하여,

  오브젝트에 이벤트가 발생했을 때 오브젝트가 쉽게 알 수 있게끔 하는 것 입니다.

  그렇다면 이제 궁금한 점은 어떻게 이런 식으로 쉽게 이벤트를 관리할 수 있는 최적화된 시스템을 코딩하는 방법이 무엇일까요?

  바로, EventManager 클래스를 통해 오브젝트가 특정 이벤트를 수신 할 수 있도록 하는 것 입니다.

  

   이 시스템은 다음의 세가지 주요한 개념에 기반 합니다.

 

• 이벤트 리스너(EventListener) : 자신이 발생시킨 이벤트를 포함한 어떤 이벤트가 발생하면 알기를 원하는 모든 오브젝트를

   리스너(listener) 라고 부릅니다. 

   실직적으로 대부분의 오브젝트는 하나 이상의 이벤트에 대한 리스너 입니다.

 

• 이벤트 포스터(EventPoster) : 리스너와 반대로 오브젝트가 이벤트 발생을 알아차린 경우, 이 오브젝트는 다른 모든 리스너가 알 수 있게

   이벤트에 대해 알려야 한다.

   하지만 설명한 의미의 진정한 포스터(발신자)가 되려며 오브젝트가 전역 레벨에서 이벤트를 발생시켜야 한다.

 

• 이벤트 매니저(EventManager) : 마지막으로, 여러 레벨에 걸쳐 계속 유지되며 전역적으로 접근이 가능한, 가장 중요한 싱글턴

   (EventManager) 오브젝트가 있다. 이 오브젝트는 리스너를 포스터에게 실질적으로 연결하는 역할을 합니다.

    이벤트 매니저는 포스터가 보낸 알림을 받고 적합한 모든 리스너에게 이벤트 형식으로 즉시 알림을 발생킵니다.


인터페이스를 통한 이벤트 관리

: 이벤트 처리 시스템의 첫째 구성요소는 바로 특정 이벤트가 발생했을 때 알림을 받는 리스너입니다.

  리스너는 EventManager에 하나 혹은 그 이상의 이벤트에 대해 스스로를 리스너로 등록해야 동작하게 됩니다.

  그런 다음, 실제로 이벤트가 발생하게 되면 함수 호출을 통해 리스너에게 바로 알림이 오게 됩니다.

  

   아래의 예제에서 SendMessage 와 BroadcastMessage가 리플렉션을 통해 제공하는 기능을 인터페이스와

   다형성을 이용해 구현할 예정 입니다. ( 구체적으로는 리스너 오브젝트를 파생할 때 인터페이스를 만들 것 입니다.)

   

C#에서 인터페이스는 껍데기뿐인 기본 추상 클래스와 같은 것입니다.
인터페이스는 클래스처럼 메소드와 함수를 하나의 템플릿과 같은 단위의 집합으로 한데 모으는 역할을 합니다.
하지만, 클래스와는 달리, 인터페이스는 함수의 이름, 반환 형식, 파라미터와 같은 함수 원형을 선언하는 것만 허용 됩니다.  

인터페이스의 역할은 각각의 파생된 클래스의 특정 형식에 대해 알 필요 없이 다형성을 통해 다른 오브젝트들로 하여금 함수를 호출할 수 있게끔 하는 것입니다.

   인터페이스를 이용하여 리스너 오브젝트를 만들면 모든 오브젝트가 이벤트 리스너가 될 수 있는 능력을 가질 수가 있습니다.


✣ 리스너 인터페이스 예시

IListener 인터페이스를 사용함으로써 클래스 상속을 사용하는 모든 오브젝트를 리스너로 만들 수 있는 능력을 가지게 되었습니다.
즉, 어떤 오브젝트든 스스로를 리스너로 선언하면 이벤트를 수신하는 것이 가능해 집니다.

IListener 인터페이스 사용하는 방법


이벤트 매니저 만들기

: 이벤트가 실제로 발생했을 때 리스너들에게 이벤트를 호출하는 것은 이벤트 매니저의 의무 입니다.

  EventManager 클래스는 지속되는 싱글턴 오브젝트로, 씬 안의 빈 게임오브젝트에 붙이고 나면 정적 인스턴스 속성을 통해

  모든 오브젝트에서 직접 접근 할 수 있게 됩니다.

 

✣ EventManager

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

// 리스너들에게 이벤트를 보내기 위한 이벤트 매니저 싱글턴
// IListener 구현과 함께 작동한다.
public class EventManager : MonoBehaviour
{
    #region C# 프로퍼티

    // 인스턴스에 접근하기 위한 Public 프로퍼티
    public static EventManager Instance
    {
        get { return instance; }
        set { }
    }
    #endregion

    #region 변수들

    // 이벤트 매니저 인스턴스에 대한 내부 참조(싱글턴 디자인 패턴)
    private static EventManager instance = null;

    // 리스너 오브젝트의 배열(모든 오브젝트가 이벤트 수신을 위해 등록되어 있다.)
    private Dictionary<EVENT_TYPE, List<IListener>> Listeners = new Dictionary<EVENT_TYPE, List<IListener>>();
    #endregion


    #region 메소드
    // 시작 시에 초기화를 위해 호출된다.

    private void Awake()
    {
        // 인스턴스가 없는 경우 현재 인스턴스를 할당한다
        if(instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject); // 씬에서 빠져나갈 때 파괴되는 것을 방지한다.
        }
        else
        {
            // 인스턴스가 이미 있다면 현재 인스턴스를 파괴한다. 싱글턴 오브젝트가 되어야 한다.
            DestroyImmediate(this);
        }
    }

    // 리스너 배열에 지정된 리스너 오브젝트를 추가하기 위한 함수
    public void AddListener(EVENT_TYPE Event_Type, IListener Listener)
    {
        // 이 이벤트를 수신할 리스너의 리스트
        List<IListener> ListenList = null;

        // 이벤트 형식 키가 존재하는지 검사한다. 존재하면 이것을 리스트에 추가한다.
        if (Listeners.TryGetValue(Event_Type, out ListenList))
        {
            ListenList.Add(Listener);
            return;
        }

        // 아니면 새로운 리스트를 생성한다
        ListenList = new List<IListener>();
        ListenList.Add(Listener);
        Listeners.Add(Event_Type, ListenList);  // 내부의 리스너 리스트에 추가한다.
    }


    // 이벤트를 리스너에게 전달하기 위한 함수
    public void PostNotification(EVENT_TYPE Event_Type, Component Sender , Object Param = null)
    {
        //  모든 리스너에게 이벤트에 대해 알린다.

        // dl dlqpsxmfmf tntlsgksms fltmsjemfdml fltmxm
        List<IListener> ListenList = null;

        // 이벤트 항목이 없으면 ,알릴 리스너가 없으므로 끝낸다.
        if (!Listeners.TryGetValue(Event_Type, out ListenList))
            return;

        // 항목이 존재한다. 이제 적합한 리스너에게 알려준다.
        for(int i = 0; i < ListenList.Count; i++)
        {
            // 오브젝트가 null이 아니면 인터페이스를 통해 메시지를 보낸다.
            if (!ListenList[i].Equals(null))
                ListenList[i].OnEvent(Event_Type, Sender, Param);
        }
    }

    // 이벤트 종류와 리스너 항목을 딕셔너리에서 제거한다.
    public void RemoveEvent(EVENT_TYPE Event_Type)
    {
        // 딕셔너리의 항목을 제거한다.
        Listeners.Remove(Event_Type);
    }

    // 딕셔너리에서 쓸모 없는 항목들을 제거 한다.
    public void RemoveRedundancies()
    {
        // 새 딕셔너리 생성
        Dictionary<EVENT_TYPE, List<IListener>> TmpListeners = new Dictionary<EVENT_TYPE, List<IListener>>();

        // 모든 딕셔너리 항목을 순회한다.
        foreach(KeyValuePair<EVENT_TYPE , List<IListener>> Item in Listeners)
        {
            // 리스트의 모든 리스너 오브젝트를 순회하며 null 오브젝트를 제거한다.
            for(int i = Item.Value.Count -1; i >= 0; i--)
            {
                //null 이면 항목을 지운다.
                if (Item.Value[i].Equals(null))
                    Item.Value.RemoveAt(i);
            }
        }

        // 새로 최적화된 딕셔너리로 교체한다
        Listeners = TmpListeners;
    }

    // 씬이 변경될 때 호출된다. 딕셔너리를 청소한다
    private void OnLevelWasLoaded(int level)
    {
        RemoveRedundancies();
    }

    #endregion
}
모노디벨롭에서 #region 과 #endregion을 이용한 코드 접어두기
: #region 과 #endregion 두 전처리기를 이용하면 코드의 가독성을 향상시키는 데 큰 도움이 되고 소스 파일을 찾는 시간을
   단축시켜줍니다.
  이 전처리기는 소스 코드의 유효성이나 실행 결과에 영향을 주지 않으면서 코드를 조직화, 구조화 시켜 줍니다.

이벤트 매니저 활용

: 씬 하나에 리스너와 포스터가 있는 실제 환경에서 동작할 EventManager클래스를 어떻게 넣을 수 있을지 살펴 봅시다.

  먼저, 이벤트를 수신 하기 위해 리스너는 EventManager 싱글턴 인스턴스에 등록 되어야 합니다.

  보통 이런 과정은 Start 함수와 같이 최초 시점에 한번만 이루어 집니다.

void Start()
{
      // 체력 변동 이벤트를 수신하기 위해 스스로를 리스너로 등록해야 한다
      EventManager.Instance.AddListener(EVENT_TYPE.HEALTH_CHANGE, this);
}

하나 이상의 리스너를 등록하고 나면 다음 예제 코드와 같이 오브젝트가 발생한 이벤트를 EventManager에 알릴 수 있게 됩니다.

public int Health
{
     get { return _health; }
     set. 
     {
           // 체력 값을 0 - 100 사이에 맞춘다.
               _health = Mathf.Clamp(value, 0 , 100);

          // 알림을 보낸다 - 체력 값이 변경되었음
          EventManager.Instance.PostNotification(EVENTYPE.HEALTH_CAHNGE , this , _health);
     }
}

마지막으로, 이벤트에 대한 알림을 보내고 나면 연관된 모든 리스너가 EventManager를 통해 업데이트 됩니다.

구체적으로 EventManager는 각 리스너의 OnEvent 함수를 호출해 다음 예제와 같이 필요에 따라 이벤트 데이터를 분석하고 응답할

기회를 리스너에게 제공 합니다.

 // 이벤트가 발생할 때 호출된다.
public void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null)
{
       // 이벤트 종류를 알아낸다.
       switch(Event_Type)
        {
           case EVENT_TYPE.HEALTH_CHANGE : 
                     OnHealthChange(Sender, (int)Param);
                     break;

           ...

델리게이트를 이용한 대안

: 인터페이스는 이벤트 처리 시스템을 구현하는 효율적이고 깔끔한 방법이지만, C# 델리게이트 라는 기능을 이용할 수도 있습니다.

  기본적으로, 함수를 생성해 이 함수의 참조를 변수 안에 저장하는 것이 가능합니다.

  이 변수를 통해 참조 형식의 변수로서 함수를 취급하는 것이 가능해 집니다.

    즉, 델리게이트를 이용하면 함수의 참조를 저장했다가 나중에 이 변수를 이용해 함수를 부를 수 있게 됩니다. 

     ( c++경우엔 함수 포인터를 통해 비슷한 동작을 구현할 수 있습니다.)

 

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

// 가능한 게임 이벤트를 모두 열거한다
// 더 많은 이벤트가 리스트에 추가된다.
public enum EVENT_TYPE
{
    GAME_INIT,
    GAME_END,
    AMMO_CHANGE,
    HEALTH_CHANGE,
    DEAD
};


// 리스너들에게 이벤트를 보내기 위한 이벤트 매니저 싱글턴
// IListener 구현과 함께 작동한다.
public class EventManager : MonoBehaviour
{
    #region C# 프로퍼티

    // 인스턴스에 접근하기 위한 Public 프로퍼티
    public static EventManager Instance
    {
        get { return instance; }
        set { }
    }
    #endregion

    #region 변수들

    // 이벤트 매니저 인스턴스에 대한 내부 참조(싱글턴 디자인 패턴)
    private static EventManager instance = null;


    // 이벤트를 위한 델리게이트 형식을 선언한다.
    public delegate void OnEvent(EVENT_TYPE Event_Type, Component Sender, Object Param = null);


    // 리스너 오브젝트의 배열(모든 오브젝트가 이벤트 수신을 위해 등록되어 있다.)
    private Dictionary<EVENT_TYPE, List<OnEvent>> Listeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();
    #endregion


    #region 메소드
    // 시작 시에 초기화를 위해 호출된다.

    private void Awake()
    {
        // 인스턴스가 없는 경우 현재 인스턴스를 할당한다
        if(instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject); // 씬에서 빠져나갈 때 파괴되는 것을 방지한다.
        }
        else
        {
            // 인스턴스가 이미 있다면 현재 인스턴스를 파괴한다. 싱글턴 오브젝트가 되어야 한다.
            DestroyImmediate(this);
        }
    }

    // 리스너 배열에 지정된 리스너 오브젝트를 추가하기 위한 함

    public void AddListener(EVENT_TYPE Event_Type, OnEvent Listener)
    {
        // 이 이벤트를 수신할 리스너의 리스트
        List<OnEvent> ListenList = null;

        // 이벤트 형식 키가 존재하는지 검사한다. 존재하면 이것을 리스트에 추가한다.
        if (Listeners.TryGetValue(Event_Type, out ListenList))
        {
            ListenList.Add(Listener);
            return;
        }

        // 아니면 새로운 리스트를 생성한다
        ListenList = new List<OnEvent>();
        ListenList.Add(Listener);
        Listeners.Add(Event_Type, ListenList);  // 내부의 리스너 리스트에 추가한다.
    }


    // 이벤트를 리스너에게 전달하기 위한 함수
    public void PostNotification(EVENT_TYPE Event_Type, Component Sender , Object Param = null)
    {
        //  모든 리스너에게 이벤트에 대해 알린다.

        // dl dlqpsxmfmf tntlsgksms fltmsjemfdml fltmxm
        List<OnEvent> ListenList = null;

        // 이벤트 항목이 없으면 ,알릴 리스너가 없으므로 끝낸다.
        if (!Listeners.TryGetValue(Event_Type, out ListenList))
            return;

        // 항목이 존재한다. 이제 적합한 리스너에게 알려준다.
        for(int i = 0; i < ListenList.Count; i++)
        {
            // 오브젝트가 null이 아니면 인터페이스를 통해 메시지를 보낸다.
            if (!ListenList[i].Equals(null))
                ListenList[i](Event_Type, Sender, Param);
        }
    }

    // 이벤트 종류와 리스너 항목을 딕셔너리에서 제거한다.
    public void RemoveEvent(EVENT_TYPE Event_Type)
    {
        // 딕셔너리의 항목을 제거한다.
        Listeners.Remove(Event_Type);
    }

    // 딕셔너리에서 쓸모 없는 항목들을 제거 한다.
    public void RemoveRedundancies()
    {
        // 새 딕셔너리 생성
        Dictionary<EVENT_TYPE, List<OnEvent>> TmpListeners = new Dictionary<EVENT_TYPE, List<OnEvent>>();

        // 모든 딕셔너리 항목을 순회한다.
        foreach(KeyValuePair<EVENT_TYPE , List<OnEvent>> Item in Listeners)
        {
            // 리스트의 모든 리스너 오브젝트를 순회하며 null 오브젝트를 제거한다.
            for(int i = Item.Value.Count -1; i >= 0; i--)
            {
                //null 이면 항목을 지운다.
                if (Item.Value[i].Equals(null))
                    Item.Value.RemoveAt(i);
            }
        }

        // 새로 최적화된 딕셔너리로 교체한다
        Listeners = TmpListeners;
    }

    // 씬이 변경될 때 호출된다. 딕셔너리를 청소한다
    private void OnLevelWasLoaded(int level)
    {
        RemoveRedundancies();
    }

    #endregion
}

MonoBehaviour 이벤트

: 유니티가 이벤트 주도적 프로그래밍을 위해 제공하는 몇몇 이벤트들을 살펴 봅시다.

  MonoBehaviour 클래스는 특정한 조건에서 자동으로 호출되는 광범위한 이벤트들을 노출하고 있습니다.

 이 함수 혹은 이벤트들은 OnGUI, OnMouseEnter, OnMouseDown, OnParticleCollision등과 같이 'On' 접두사로 시작하는

 이름을 가지고 있습니다. 

 이번에는 일반적인 이벤트 형식에 대해 몇 가지 자세한 내용을 다뤄 보도록 하겠습니다.

 

☐  마우스와 탭 이벤트

: 유용한 이벤트 모음 중 하나로 마우스 입력과 터치 입력 이벤트 모음이 있습니다.

  이 이벤트에는 OnMouseDown, OnMouseEnter, OnMouseExit 가 포함 됩니다.

 

  이 이벤트들의 성공 여부는 마우스 이벤트가 검출될 충돌체 컴포넌트가 오브젝트의 크기와 유사한 크기로 설정되었는지 여부에 

  달려 있습니다.

  오브젝트에 충돌체가 붙어있지 않다면 마우스 이벤트가 발생하지 않는다는 얘기 입니다.

    하지만, 충돌체가 붙어있지만 마우스 이벤트가 발생하지 않을 때가 있는데, 활성화 된 카메라의 현재 시점에서 클릭을 받길 

  원하는 오브젝트를 다른 오브젝트(충돌체를 가진)가 가리고 있는 경우 입니다.

   해결 하려면 앞쪽에 위치한 오브젝틀에게 IgnoreRaycast 레이어를 할당해 레이캐스트 물리 연산을 무시하도록 하면 됩니다.


✣ 입력 이벤트 예제

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

public class ManualMouse : MonoBehaviour
{
    // 이 오브젝트에 붙어있는 충돌체를 얻는다.
    private Collider Col = null;

    private void Awake()
    {
        // 충돌체 얻어오기
        Col = GetComponent<Collider>();
    }


    // Start is called before the first frame update
    void Start()
    {
        //StartCoroutine
    }

    public IEnumerator UpdateMouse()
    {
        // 교차가 발생했는가
        bool bIntersected = false;

        // 버튼이 눌려졌는가
        bool bButtonDown = false;


        // 무한 루프
        while(true)
        {
            // 마우스 화면 좌표를 X 와 Y 값으로 얻는다
            // 복수의 카메라를 사용하는 경우라면 다른 카메라를 사용해야 할 수도 있다.
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            // 이 충돌체로의 충돌을 검사하는 선을 조사한다(레이캐스트)
            if (Col.Raycast(ray, out hit, Mathf.Infinity))
            {
                // 교차가 발생함 - 이전 루프에서 교차하지 않았으면 마우스 진입 메시지를 보낸다.
                if (!bIntersected)
                {
                    SendMessage
                        ("OnMouseEnter",
                        SendMessageOptions.DontRequireReceiver);
                }

                bIntersected = true;

                // 마우스 이벤트를 검사한다.
                if(!bButtonDown &&Input.GetMouseButton(0))
                {
                    bButtonDown = true;
                    SendMessage(
                        "OnMouseDown",
                        SendMessageOptions.DontRequireReceiver);
                }
                else if (bButtonDown && !Input.GetMouseButton(0))
                {
                    bButtonDown = false;
                    SendMessage("OnMouseDown", SendMessageOptions.DontRequireReceiver);
                }
            }
            else
            {
               // 이전 루프에서 교차한 경우 마우스 진출 메시지를 보낸다.
               if(bIntersected)
                {
                    SendMessage("OnMouseExit",
                        SendMessageOptions.DontRequireReceiver);
                }

                bIntersected = false;
                bButtonDown = false;
            }

            // 다음 프레임까지 대기 한다.
            yield return null;
           
        }
    }
}

☐  코루틴

: 코루틴은 특별한 종류의 함수 입니다.
  코루틴은 메인 게임 루프에서 병렬이나 비동기로 실행되는 것처럼 보이지는 측면을 고려할 때 스레드(thread) 처럼 동작한다고 할 수 있습니다.
   즉, 코루틴을 실행시키면 백그라운드에서 실행되는 것 처럼 보입니다.
  코루틴을 실행하면 일반적인 함수 처럼 이전 동작이 멈추거나 코루틴 함수가 완료될 때까지 기다리고 있지 않습니다.
  
  코루틴은 IEnumerator 형식을 반환해야 하는데, 최소한 하나의 yield문을 포함하고 있어야 하고 StartCoroutine으로 실행되어야 합니다.
 
  -  yield return new WaitForSeconds(x) : x 초 만큼 실행을 멈추었다가 시간이 지나면 다음 줄의 실행을 계속 합니다.
  -  yield return null : 현재 프레임에서 실행을 멈추었다가 다음 프레임에서 이후의 코드를 실행하도록 합니다.