[Unity] 23. 코루틴 최적화 (Coroutine Cache) & 쉐이더

치치·2025년 3월 27일
0

Unity

목록 보기
25/27
post-thumbnail

자주 사용하는 코루틴을 미리 정의해두고 사용해보자
딕셔너리에 내가 원하는 시간을 key값으로, 생성할 waitForSeconds를 벨류값으로 저장

과정

  1. 코루틴캐시 클래스 생성 -> MonoBehaviour를 상속받지 않는다

  2. 딕셔너리 생성 (키, 벨류값)

  3. 키값 : float 시간
    → 내가 찾고자 하는 시간 값이 있으면 꺼내서 사용
    → 만약 찾고자하는 키 값이 없으면 새로 값 추가 new WaitForSeconds

  4. 벨류 값 : waitForSeconds 타입

(추가하면 나중에 또 쓸때도 딕셔너리에 이미 추가되어있어서 바로 꺼내서 사용)
-> 즉, 코루틴을 재사용 할 수 있다



이전에 매번 새로 new WaitForSeconds로 메모리 할당했던 방식
-> 미리 WaitForSeconds를 정의해두고 사용했던 방식
-> 에서 더 나아간 방식

딕셔너리에 키 값과 벨류값을 저장해두고, 만약 있는 값이면 사용하고 없는 값이면 딕셔너리에 추가한다

CoroutineCache 스크립트

클래스 생성 & 딕셔너리 생성

  • MonoBehaviour를 상속받지 않는 클래스 생성 (일반C# 스크립트) -> 정적함수를 생성하면 클래스 이름으로 바로 접근이 가능해진다

  • 정적타입의 딕셔너리 생성 <키, 벨류값>
    Key : float 타입의 시간
    Value : WaitForSeconds 타입의 객체 참조(주소값)

  • C#에서는 컬렉션 타입들은 참조타입이다

  • C++과는 다르게 C#은 new를 사용하여 동적 할당한 뒤, 변수가 '할당된 객체'를 참조하는 형식이다

WaitForSeconds 타입을 반환하는 정적 함수 생성

  • WaitForSeconds를 반환하는 함수 생성
    -> 정적 함수로 생성하여 다른 스크립트에서 사용할 수 있게 한다 (매개변수로 원하는 float 시간 값)

  • 내가 찾고자 하는 키 값이 없을때만 딕셔너리에 추가할 것이다 -> 이미 있는 키 값이 라면 바로 반환


📌 TryGetValue

  • TryGetValue 메서드는 해당하는 키 값이 있다면 키 값에 해당하는 벨류값을 out 매개변수에 할당하고, true를 반환한다
  • 만약 찾고자하는 키 값이 아니라면, out 매개변수에 기본값 할당한 후 false를 반환한다

ex) ✅ TryGetValue를 사용하면 키 값이 만약 있다면 대입을 진행하지 않고 참조변수 waitForSeconds에 벨류값 할당한 뒤 바로 반환


📌 ContainsKey

  • 해당하는 키 값이 존재할 경우 true를, 없으면 false를 반환한다

  • dictionary.TryGetValue(time, out waitForSeconds) 로 키 값이 있다면 바로 waitForSeconds 참조변수에 벨류값이 할당되고 조건문을 시행하지 않고 반환된다**

ex) ✅ ContainsKey를 사용하면 키 값이 있든 없든 일단 대입이 한번은 꼭 되기 때문에 TryGetValue와 비교해서 상대적으로 비효율적이다



딕셔너리 키 값 추가 과정

  • 만약 찾고자 하는 키가 없다면 (false라면)
    -> WaitForSeconds를 담을 waitForSeconds 참조변수를 생성한다

  • 딕셔너리에 키 값과 벨류값을 추가한다
    -> ✅ 벨류값에 new WaitForSeconds를 통해 객체를 만들고 그 객체의 참조(주소값)를 담는다
    -> ✅ 실제 객체는 힙에 만들어져있고, 스택에서 이 객체의 주소값을 참조한다

  • 참조변수에 키값에 해당하는 벨류값을 담는다
    -> WaitForSeconds 타입의 벨류값을 반환한다

  • dictionary[key]; : 키를 사용해 벨류값에 접근 가능

  • C#에서 클래스는 참조타입임!! new WaitForSeconds(time)는 참조(주소값)을 의미함



CoroutineCache 스크립트

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class CoroutineCache
{
    // 참조타입이라 new로 생성 (C++)
    static Dictionary<float, WaitForSeconds> dictionary = new Dictionary<float, WaitForSeconds>();
    
    // 정적 함수 생성하여 다른 클래스에서 사용

    // time = 키 값
    // WaitForSeconds 타입을 반환
    public static WaitForSeconds WaitForSeconds(float time)
    {
        // 내부에는 정적인 것만 가능 (정적변수)

        // 딕셔너리 키값에 해당 시간만큼의 WaitForSeconds 생성

        // TryGetValue 사용하여 키 값이 없다면 추가한뒤 반환
        // 키 값이 있다면 그냥 반환
        
        // 참조변수
        WaitForSeconds waitForSeconds;

        if(dictionary.TryGetValue(time, out waitForSeconds) == false)
        {
            dictionary.Add(time, new WaitForSeconds(time));

            // 추가 한 뒤에 참조해야한다 -> 추가 하기 전까지는 키벨류가 null 상태

            waitForSeconds = dictionary[time];
        }

        return waitForSeconds;
    }
}


플라이웨이트 패턴과 코루틴 최적화 관계

플라이웨이트 패턴 : 객체의 중복을 줄이기 위해 공유 가능한 객체를 사용하여 메모리 소비를 최소화하는 패턴



쉐이더 사용하기

쉐이더에 대해서는 나중에 더 알아보도록 하자

  • Standard Surface Shader 선택

  • 내가 쉐이더를 적용하고 싶은 MaterialsShader 타입을 생성한 쉐이더의 이름(Custom)으로 설정

  • 만약 3D모델링에 textures는 없고 Materials만 적용되어 있는 경우, Materials에 쉐이더를 적용하면 텍스쳐가 다 사라져서 하얗게 보인다
    -> 되도록 텍스쳐 파일도 있는 에셋을 가져오자


Surface.Shader

Shader "Custom/Surface"
{
 Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Curvature("Curvature", float) = 0.001
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert addshadow
        uniform sampler2D _MainTex;
        uniform float _Curvature;

        struct Input
        {
            float2 uv_MainTex;
        };

        void vert(inout appdata_full v)
        {
            float4 worldSpace = mul(unity_ObjectToWorld, v.vertex);
            worldSpace.xyz -= _WorldSpaceCameraPos.xyz;
            worldSpace = float4(0.0f, (worldSpace.z * worldSpace.z) * -_Curvature, 0.0f, 0.0f);
    
            v.vertex += mul(unity_WorldToObject, worldSpace);
        }

        void surf(Input IN, inout SurfaceOutput o)
        {
            half4 c = tex2D(_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }

        ENDCG        
    }
    FallBack "Mobile/Diffuse"
}


레벨 디자인

플레이 시간이 지날수록 장애물이 활성화 되는 간격이 점점 줄어들게 만들어보자

  • 전체적인 스피드는 5초마다 2.5씩 증가

  • 5초마다 장애물의 생성 속도가 -0.25씩 감소 (0.25만큼 더 빨리 생성)


코드 자세히

  • 나중에 확장을 대비해서 TimeManager는 싱글톤을 상속받는다

  • 초기 시간 2.5 / 한계 시간 0.5

  • activetime 변수를 프로퍼티를 사용하여 읽기전용으로 만든다
    -> 캡슐화 적용

  • 코루틴을 사용하여 GameManger의 상태가 true이고, 초기 시간이 한계 시간보다 크다면 반복문을 실행
    -> 5초마다 activetime -= 0.25f;

  • 위에서 만들어본 CoroutineCache 스크립트의 WaitForSeconds 함수를 사용하여 반환된 waitForSeconds를 사용할 수 있다
    -> **WaitForSecondsMonoBehaviour를 상속받지 않는 클래스의 정적 함수기 때문에 클래스 이름으로 함수를 바로 호출할 수 있다

  • 게임씬(1)이 로드될때 마다 코루틴을 실행
    -> 타이틀씬으로 갔다가 게임씬이 다시 로드될때 activetime은 초기화 되어야 하기 때문에 2.5로 다시 설정한다

  • 씬 이벤트를 등록한다

  • 원하는 곳에서 해당 activetime을 호출해서 사용한다
    장애물이 생성되는 ObstacleManager에서 호출
    -> TimeManager가 싱글톤이기 때문에 인스턴스를 통해 접근 가능



TimeManager 스크립트

코루틴의 인자로 들어가는 시간을 일정 시간이 지나면서 점점 줄이기 위해 사용되는 스크립트

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

public class TimeManager : Singleton<TimeManager>
{
    [SerializeField] float activetime = 2.5f;

    [SerializeField] float limittime = 0.5f;

    // 읽기 전용 
    public float activeTime { get { return activetime; } }

    private void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    IEnumerator Decrease()
    {
        while(GameManager.Instance.State && activeTime > limittime)
        {
            yield return CoroutineCache.WaitForSeconds(5.0f);

            activetime -= 0.25f;
        }
    }

    // 게임씬(1)이 활성화 되면 함수 실행
    void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
    {
        activetime = 2.5f;

        if (scene.buildIndex == 1)
        {
            StartCoroutine(Decrease());
        }
    }

    private void OnDisable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }
}

레벨 디자인 영상

https://youtu.be/mOwYJ7ENk0g

profile
뉴비 개발자

0개의 댓글