Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

물에 사는 벌레

유니티 스타일의 코루틴 구현하기 본문

C#

유니티 스타일의 코루틴 구현하기

물벌레 2019. 7. 14. 03:02

해당 글은 유니티의 코루틴 사용법과 C#의 IEnumerator 사용법을 알고 있다는 가정하에 작성한 글입니다.

유니티 코루틴을 모르시는 분은 여기 안 오실 테고, C#의 IEnumerator 사용법이 미숙한 분은 IEnumerator 설명 글을 읽고 와 주세요.


우선 유니티와 비슷한 스타일로 만들기 위해 유니티의 코루틴 코드 작성법을 확인해보자.

 

코루틴 선언은 아래와 같은 방식으로 한다.

yield return ... 문을 이용하여 대기할 수도 있다.

    IEnumerator WaitAndPrint()
    {
        // suspend execution for 5 seconds
        yield return new WaitForSeconds(5);
        print("WaitAndPrint " + Time.time);
    }

 

특이하게도 코루틴 내부에서 다른 코루틴을 실행하여 반환하면 해당 코루틴이 끝날 때까지 대기하게 된다.

    IEnumerator WaitAndPrint()
    {
        // suspend execution for 5 seconds
        yield return new WaitForSeconds(5);
        print("WaitAndPrint " + Time.time);
    }

    IEnumerator Start()
    {
        print("Starting " + Time.time);

        // Start function WaitAndPrint as a coroutine
        yield return StartCoroutine("WaitAndPrint");
        print("Done " + Time.time);
    }

 

코루틴 대기 조건을 사용자가 직접 정의할 수도 있다.

using UnityEngine;

public class WaitForMouseDown : CustomYieldInstruction
{
    public override bool keepWaiting
    {
        get
        {
            return !Input.GetMouseButtonDown(1);
        }
    }

    public WaitForMouseDown()
    {
        Debug.Log("Waiting for Mouse right button down");
    }
}

 

코루틴 실행은 아래와 같은 방식으로 한다.

coroutine = WaitAndPrint(2.0f);
StartCoroutine(coroutine);
// 혹은
StartCoroutine(WaitAndPrint(2.0f));

 

마지막으로 코루틴 제어 함수로는 StartCoroutine, StopCoroutine, StopAllCoroutine 등이 있다.

위와 최대한 비슷하게 구현해 보자.


대략 생각해보면, 매 업데이트마다 모든 코루틴을 실행할 것이며, 코루틴에 작성된 대기 조건 클래스를 검사할 것이다.

 

우선 코루틴 대기 조건부터 구현해 본다.

    abstract class CustomYieldInstruction
    {
        public abstract bool keepWaiting { get; }
    }

해당 클래스를 상속받아 현재 대기할 것인지 아닌지에 대한 keepWaiting 메서드를 정의해 주면 된다.

 

간단하게 유니티에서 제일 많이 사용되는 두 클래스를 구현해 보았다.

    class WaitForSeconds : CustomYieldInstruction
    {
        const float TicksPerSecond = TimeSpan.TicksPerSecond;
        long m_end;

        public WaitForSeconds(float seconds)
        {
            m_end = DateTime.Now.Ticks + (long)(TicksPerSecond * seconds);
        }

        public override bool keepWaiting
        {
            get { return DateTime.Now.Ticks < m_end; }
        }
    }

여기서 keepWaiting은 처음 클래스가 만들어진 시간 + 입력한 시간보다 현재 시간이 작은 경우 대기하라는 명령을 반환한다.

 

    class WaitForUpdate : CustomYieldInstruction
    {
        bool m_nextframe_has_passed;

        public WaitForUpdate()
        {
            m_nextframe_has_passed = false;
        }

        public override bool keepWaiting
        {
            get
            {
                if (!m_nextframe_has_passed)
                {
                    m_nextframe_has_passed = true;
                    return true;
                }
                return false;
            }
        }
    }

여기서 keepWaiting은 처음 접근했을 때는 다음 프레임을 지났다는 플래그를 활성화 시키며, 다음 접근에서부터는 대기하지 말라는 명령을 반환한다.


다음으로 코루틴 매니저 클래스를 구현한다.

여기서 모든 코루틴을 처리할 것이다.

 

우선 선언부터 한다.

class CoroutineManager

 

다음으로 코루틴 매니저는 한 개면 충분하기 때문에 싱글톤으로 작성한다.

        private static CoroutineManager _instance;
        private CoroutineManager() { }

        List<IEnumerator> m_routines;

        static CoroutineManager()
        {
            _instance = new CoroutineManager();
            _instance.m_routines = new List<IEnumerator>();
        }

m_routines 리스트에 코루틴들을 담을 예정이다.

 

다음으로 유니티에 존재하는 클래스 제어 메서드를 구현한다.

        public static IEnumerator StartCoroutine(IEnumerator routine)
        {
            _instance.m_routines.Add(routine);
            return routine;
        }

        public static bool StopCoroutine(IEnumerator routine)
        {
            return _instance.m_routines.Remove(routine);
        }

        public static void StopAllCoroutines()
        {
            _instance.m_routines.Clear();
        }

        public static bool IsRunning(IEnumerator routine)
        {
            return _instance.m_routines.Contains(routine);
        }

        public static int Runnings()
        {
            return _instance.m_routines.Count;
        }

전부 코루틴 리스트에 관련된 코드이며 한 줄짜리 메서드이기 때문에 굳이 설명은 하지 않는다.

 

마지막으로 코루틴들을 내부에서 처리할 메서드를 만들어야 한다.

나는 각 코루틴별로 처리해 줄 생각이다.

고로 Process(IEnumerator) 라는 개별 코루틴 처리 메서드와 이 Process메서드를 이용하여 리스트의 코루틴을 넣어 처리해주는 Update() 메서드를 만든다.

        private static bool Process(IEnumerator routine)
        {
            do{
                object current = routine.Current;
                if (current is IEnumerator)
                {
                    IEnumerator other_routine = current as IEnumerator;
                    if (!Process(other_routine)) return false;
                    else continue;
                }
                else if (current is CustomYieldInstruction)
                {
                    CustomYieldInstruction yieldInstruction = current as CustomYieldInstruction;
                    if (yieldInstruction.keepWaiting) return false;
                }
            }
            while (routine.MoveNext());

            if (!routine.MoveNext()) _instance.m_routines.Remove(routine);
            return true;
        }

        public static void Update()
        {
            for(int i = 0; i < _instance.m_routines.Count; i++)
            {
                Process(_instance.m_routines[i]);
            }
        }

말했다시피 Update에서 리스트에 존재하는 모든 코루틴을 Process 메서드에 넣어 처리한다.

참고로 Process 내부에서 리스트의 코루틴을 제거하는 작업도 존재하기 때문에 foreach를 사용해서는 안 된다.

 

그리고 프로세스 메소드에서의 동작은 이렇다.

인수로 들어온 코루틴이 더 이상 진행할 코드가 없다면(MoveNext() == false 혹은 코루틴 내부에서 yield break;를 호출하는 경우) true를 반환하고 리스트에서 제거한다.

더 진행할 코드가 있다면 계속 루프를 돌며 대기 조건이 존재할 때, 대기하라는 명령을 반환받으면 false를 반환하고 메소드를 종료한다.

 

순서대로 생각해보면...

처음 들어온 코루틴의 Current는 null 일 것이므로 아무런 처리도 하지 않고 MoveNext 메서드가 실행되어 다시 검사를 하게 된다.

 

이후, Current가 IEnumerator라는 것은 코루틴 내에서 다른 코루틴을 실행 및 반환하고 있다는 이야기이므로 반환된 코루틴을 다시 Process 메소드에 넣고 돌린다. 넣은 코루틴이 false를 반환하는 경우 대기하라는 이야기이므로 외부의 코루틴 역시 대기한다. 

재귀함수 설명 어려움...

 

혹은 커스텀 대기명령(CustomYieldInstruction)이 반환된 경우에, 대기하라는 명령(false)이 반환된 경우 역시 false를 반환하고 메서드를 종료한다.

 

이 메소드가 메 업데이트마다 실행되므로 계속 대기명령 클래스의 keepWaiting 메서드를 부를 것이고, 그에 따라 언젠간 대기명령 반환이 false가 될 것이며, 마침내 루프를 나와 참을 반환하고 리스트에서 제거될 것이다.


이제 작동 테스트를 해 보자.

    class Program
    {
        static IEnumerator routine1()
        {
            Console.WriteLine("routine1: 1초 대기");
            yield return new WaitForSeconds(1);
            Console.WriteLine("routine1: 1초 대기 종료");

            Console.WriteLine("routine1: routine2 대기");
            yield return CoroutineManager.StartCoroutine(routine2());
            Console.WriteLine("routine1: routine2 대기 종료");

            Console.WriteLine("routine1: 2초 대기");
            yield return new WaitForSeconds(2);
            Console.WriteLine("routine1: 2초 대기 종료");

            Console.WriteLine("routine1: 3초 대기");
            yield return new WaitForSeconds(3);
            Console.WriteLine("routine1: 3초 대기 종료");
        }

        static IEnumerator routine2()
        {
            Console.WriteLine("routine2: 1초 대기");
            yield return new WaitForSeconds(1);
            Console.WriteLine("routine2: 1초 대기 종료");

            Console.WriteLine("routine2: 2초 대기");
            yield return new WaitForSeconds(2);
            Console.WriteLine("routine2: 2초 대기 종료");

            Console.WriteLine("routine2: 3초 대기");
            yield return new WaitForSeconds(3);
            Console.WriteLine("routine2: 3초 대기 종료");
        }

        static void Main(string[] args)
        {
            CoroutineManager.StartCoroutine(routine1());
            while (CoroutineManager.Runnings() > 0) CoroutineManager.Update();
        }
    }
    
// 출력
// routine1: 1초 대기
// routine1: 1초 대기 종료
// routine1: routine2 대기
// routine2: 1초 대기
// routine2: 1초 대기 종료
// routine2: 2초 대기
// routine2: 2초 대기 종료
// routine2: 3초 대기
// routine2: 3초 대기 종료
// routine1: routine2 대기 종료
// routine1: 2초 대기
// routine1: 2초 대기 종료
// routine1: 3초 대기
// routine1: 3초 대기 종료
// 계속하려면 아무 키나 누르십시오 . . .

메인 메소드에서는 존재하는 코루틴이 0개가 될 때까지 업데이트를 계속 호출한다.

루틴1에서는 1초 대기 후 루틴2를 호출하는데 유니티와 같이 새로 실행된 루틴2가 종료될 때까지 기다리고, 마저 자신의 처리를 끝 내는 모습을 볼 수 있다.

'C#' 카테고리의 다른 글

C#의 IEnumerator에 대해 알아보자  (1) 2019.07.13
Comments