C#, Unity - 코루틴 Yield 최적화 하기(+IEqualityComparer)

Justin·2022년 10월 22일
2

유니티

목록 보기
9/9
post-thumbnail

왜 이걸 공부하게 되었는가?


게임을 제작하다보면 아래와 같이 일정 시간 딜레이를 줄 때 코루틴을 자주 사용하게 된다. 하지만 여기서 yield 구문과 함께 new 키워드를 사용하게 되면 인스턴스가 일어나 GC의 먹이가 된다.

private IEnumerator Delay()
    {
        yield return new WaitForSeconds(1f);
    }

한 번만 사용한다면 상관 없지만 아래와 같이 반복되는 상황에서 여러번 사용될 때는 지속적으로 인스턴스 되면서 최적화 이슈가 생길 수 있다.

private IEnumerator Delay()
    {
    	for(int i = 0; i < 100; i++)
        {
			yield return new WaitForSeconds(1f);
        }
    }

그래서 보통 미리 캐싱해두고 사용한다. 나도 주로 아래와 같이 캐싱해두고 사용을 자주 했었다.

private IEnumerator Delay()
    {
    	WaitForSeconds waitflag1 = new WaitForSeconds(1f);
    	for(int i = 0; i < 100; i++)
        {
			yield return waitflag1;
        }
    }


그러나 다른 클래스에서도 비슷한 값을 캐싱하는 경우(중복)가 있었고, 그때 마다 반복 작업을 하기가 귀찮았다. 그래서 더 범용성있게 사용할 수 있게 static class를 하나 만들어두었다.


처음에는 Static Class를 하나 만들어 미리 캐싱을 잔뜩해놓고 사용하는 간단한 구조로 제작을 했었다.

하지만 제작을 해놓고 보니 내가 어떤 값이 필요한지는 떄에 따라 다르기도 하고, 사용하지 않는 값들도 많이 생겨서 다른 방안을 찾아봤다.

역시나 내가 한 고민은 이미 다른 사람들도 했던 것이고 굉장히 코드를 잘 짜놓은게 있어서 이해하면서 따라해보았다.



해결방안


우선 WaitForEndOfFrame, WaitForFixedUpdate 같은 경우는 float 값에 따라 변동하지 않기에 하나만 생성해두고 사용한다.

다른 코드를 보면 프로퍼티로 감싸둔 코드도 있는데 readOnly를 사용하여 값이 변하지 않게 설정해주는 방법이 참신해서 따라해보았다.

static class YieldCache
{
    public static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
    public static readonly WaitForFixedUpdate WaitForFixedUpdate = new WaitForFixedUpdate();

다음은 이제 진짜 자주 사용하는 WaitForSeconds를 캐싱하는 방법인데 float타입을 key, WaitForSeconds 타입을 value로 설정한 Dictionary를 선언한다.

 private static readonly Dictionary<float, WaitForSeconds> 
 _timeInterval = new Dictionary<float, WaitForSeconds>(new FloatComparer());

여기서 IEqualityComparer라는 인터페이스를 적용한 FloatComparer()클래스를 지정하면 박싱이 발생하지 않게 해주며 의도치 않게 가비지가 생성되는 것을 방지할 수 있다.


class FloatComparer : IEqualityComparer<float>
    {
        bool IEqualityComparer<float>.Equals(float x, float y)
        {
            return x == y;
        }
        int IEqualityComparer<float>.GetHashCode(float obj)
        {
            return obj.GetHashCode();
        }
    }

이제 선언해둔 Dictionary를 `WaitForSeconds()` 함수에서 이미 캐싱된게 있으면 불러와서(`TryGetValue`) 넣어주고 없다면 새로 넣어준 후 리턴해주는 효율적인 구조가 완성된다.

public static WaitForSeconds WaitForSeconds(float seconds)
    {
        WaitForSeconds wfs;
        if (!_timeInterval.TryGetValue(seconds, out wfs))
            _timeInterval.Add(seconds, wfs = new WaitForSeconds(seconds));
        return wfs;
    }

Dictionary의 Value값을 찾을 때 ContainKey() 보다 TryGetValue()를 사용하는 것이 좋다. 값을 참조하는 경우 TryGetValue가 더 빠르고, 단순하게 Key 존재 여부를 파악하는 것에도 큰 차이가 없기도하니 웬만해서는 TryGetValue가 좋다



결론

static으로 설정해두었기에 원하는 곳에서 아래와 같이 사용하면 기존처럼 사용할 때마다 일일이 캐싱해두는 것 보다 훨씬 간편하고 효율적인 구조이다.


코드 전문

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

static class YieldCache
{
    class FloatComparer : IEqualityComparer<float>
    {
        bool IEqualityComparer<float>.Equals(float x, float y)
        {
            return x == y;
        }
        int IEqualityComparer<float>.GetHashCode(float obj)
        {
            return obj.GetHashCode();
        }
    }

    public static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
    public static readonly WaitForFixedUpdate WaitForFixedUpdate = new WaitForFixedUpdate();

    private static readonly Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(new FloatComparer());
    private static readonly Dictionary<float, WaitForSecondsRealtime> _timeIntervalReal = new Dictionary<float, WaitForSecondsRealtime>(new FloatComparer());

    public static WaitForSeconds WaitForSeconds(float seconds)
    {
        WaitForSeconds wfs;
        if (!_timeInterval.TryGetValue(seconds, out wfs))
            _timeInterval.Add(seconds, wfs = new WaitForSeconds(seconds));
        return wfs;
    }

    public static WaitForSecondsRealtime WaitForSecondsRealTime(float seconds)
    {
        WaitForSecondsRealtime wfsReal;
        if (!_timeIntervalReal.TryGetValue(seconds, out wfsReal))
            _timeIntervalReal.Add(seconds, wfsReal = new WaitForSecondsRealtime(seconds));
        return wfsReal;
    }
}

출처

도움 받은 곳 1
도움 받은 곳 2
도움 받은 곳 3

profile
인디 게임을 만들며 공부하고 있습니다.

0개의 댓글