본문 바로가기

Unity 개발 공부

[내배캠] 본캠 45 문법 복습(컴포넌트찾기) , 코드 최적화

기존 문법 복습

FindObjectOFType<T>()
씬(현재 로드된 모든 게임오브젝트)에서 T타입의 컴포넌트를 처음 찾는 메서드

cameraShake = FindObjectOFType<CameraShake>(); : 씬 어딘가에 붙어있는 CameraShake 컴포넌트를 찾아
cameraShake 변수에 할당
씬전체를 훑기 때문에 성능상 비용이 크고,
여러개가 있을 경우 첫번째로 찾은것만 반환하므로 의도치않은 참조가 될수도 있다.
대안으로는 
GetComponentInChildren<T>() / GetComponent<T>() 로 구조를 명확히하거나, 미리 인스펙터에 
드래그앤드롭 참조를 연결해두는 방식을 권장한다.

GetComponent vs TryGetComponent
TryGetComponent<T>(out T comp)의 경우
반환값이 bool (성공여부)이며
out 파라미터에 out T comp 로 컴포넌트를 전달한다. 
즉 참이라면(해당컴포넌트가 있다면), T comp = GetComponent<T>(); 로 할당이 된다.
out 방식을 써서 약간 더 최적화된다.

원래라면
var rb = GetComponent<Rigidbody>();
if( rb ! = null) { ...}

이지만
if(TryGetComponent<Rigidbody>(out var rb)) { ...}
으로도 할수있다는것이다.
장점으로 코드가 간격해지고 분기처리 깔끔하고 GC 할당이 없다.

 

--------------

코드 최적화/ 프로파일러 강좌.

최적화란 개발 처음부터해야한다. 

c# 최적화가 가장중요
리소스 최적화
렌더링 최적화
가 있다면,

c# 최적화에 대해서 다뤄보자.

GC(가비지컬렉션) 최적화
GC는 메모리 관리를 자동으로 처리하지만, 성능에 영향을 준다.

Unmanaged language : 프로그램단계에서 직접관리안하는 언어: c, c++ , 개발자가 직접 할당 및 해제를 해줘야함. 
초보개발자가 접근하기에 약간의 난이도가있음.
managed language : 메모리 할당, 해제를 자동적으로 해준다. c#이 대표적이고, java도 그러하다.

GC를 최소화하기위해 객체생성과 삭제를 줄이고, 객체 풀링을 활용하자.
특정시점에 사용가능한 메모리로 바꿔주는 시점이있는데, 이때 프레임드랍이 일어날 수 있다.

Component 캐싱처리 
자주 사용하는 컴포넌트는 start, awake 등에서 변수에 할당하고 변수를 통해 컴포넌트 접근하는게 좋다.
GetComponent<T>()는 호출을 최소화하고 필요한 컴포넌트를 미리 캐싱한다.
rigidbody 같은애들...

Tag 비교
충돌 콜백함수에서 Tag 비교시 CompareTag를 사용하여 성능을 향상가능
gameObject.tag == "TagName" 대신
gameObject.CompareTag("TagName")을 사용한다.

Raycast 촤적화
Raycast 호출을 최소화하고, 필요한 경우 레이어 마스크를 사용하여 검출범위를 제한한다.
Physics.Raycast(호출하는 순간마다 GC를 발생시킴)
대신 Physics.RaycastNonAlloc을 사용하여 메모리 할당을 줄이는게 좋다. 

Animator 최적화
State의 hash를 사용하여 Animator의 성능을 향상시킬수있음.
private static readonly int HashWalk = Animator.StringToHash("Walk");
animator.SetBool(HashWalk, true); 
내부적으로 해쉬로 바꾸는게 무겁다. 따라서 코드로 바꿔준후 해당 코드로 쓰자.

컬렉션 최적화
List<T>, Dictonary<Tkey, Tvalue> ... 등의 컬렉션을 상황에 맞게 선택하면 좋다.

문자열 최적화
문자열도 조합시에, StringBuilder를 사용하여 성능 향상가능.
불필요한 문자열 생성과 삭제를 피하고, 문자열 비교시 StringComparison 옵션을 활용하자.
+로 문자열 조합하면 GC가 발생한다.


Debug 코드 제거
디버그용 코드는 최적화 과정에서 반드시 제거.
조건부 컴파일 사용도 좋다. [Conditional] 어트리뷰트 사용하여 빌드때 빼버릴수있음.
디버그 로그 클래스를 만들어서 컴파일 시점에서 빠지도록 설정해주면좋다...

LINQ 최적화
LINQ는 코드 가독성이 높지만 성능에 영향을 준다.
필요한 경우 LINQ 대신 반복문을 사용하거나, 쿼리 최적화를 고려한다.
모바일프로젝트에선 될수 있으면 쓰지않는게 좋다..



window - analysics - profiler
들어가서
cpu usage 보면
각 카테고리를 끄고킬수있다. 
Script와 GC만 켜놓고 우선 보자.
하단부 Timeline을
Hierarchy로 바꿔주자.
calls로 오른쪽 바꿔주자.
deep profile 눌러주자.

순간적으로 피크치는 주파수 부분이 15프레임 이하까지 떨어지는 구간이다.
GC Alloc를 보면 할당된 메모리를 볼수있는데
PlayerLoop가 실행파일이 돌아가는 런타임 퍼포먼스고
EditorLoop는 실행전 퍼포먼스다.

Profiler(Standalone Process)는 별개의 파일을 띄우기때문에 editorloop는 없다.. 이게 좀더 정확하게 봐주기는한다.

안에 GC가 높은구간을 들어가서 보면...
타고타고 들어가서 선택후 Calls부분을 보면 GC.Collect가 보이는데 사용불가능한 메모리를 사용가능한 메모리로 돌리는것이다.
GC.Alloc는 할당이다.

무식한 테스트법으로
    private string msg;

    private void Update()
    {
        for(int i = 0; i < 100; i++)
        {
            msg += $"Message {i}\n";
        }

        Debug.Log(msg);
    }
이렇게했을때 확인이가능하다.

이걸 최적화해보자.
    private string msg;
    private StringBuilder sb = new StringBuilder(1200);

    private void Update()
    {
        sb.Clear();
        for(int i = 0; i < 100; i++)
        {
            sb.Append("Message ").Append(i).Append("\n"); // AppendLine이란 것도있음.

            //msg += $"Message {i}\n";
        }

        Debug.Log(sb);
    }
이렇게 바꿔보자.

append 를 할경우엔 GC가 발생하지않았다.
따라서 프레임드랍을 덜었다.


이번에는 RayCast 최적화를 해보자.
    private Enemy GetClosestEnemy()
    {
        //GC
        Collider[] enemies = Physics.OverlapSphere(transform.position, 10.0f, 1 << 8);

        //GC
        var enemiesList = enemies.OrderBy(enemy => Vector3.Distance(transform.position, enemy.transform.position)).ToList();

        return (enemiesList.Count > 0) ? enemiesList[0].GetComponent<Enemy>() : null;
    }

    private void Update()
    {
        Enemy target = GetClosestEnemy();
        if(target != null)
        {
            transform.LookAt(target.transform.position);
        }
    }

이걸 GC가 안나오게 바꿔보자.

    private Collider[] buffer = new Collider[50]; 
    private Enemy GetClosestEnemy()
    {
       int count =  Physics.OverlapSphereNonAlloc(transform.position, 15f, buffer, 1 << 8);
        
        if (count == 0) return null;

        //가장 가까이 있는 적 찾기위한 변수
        float minDistance = float.MaxValue;
        Enemy closestEnemy = null;

        for(int i = 0; i < count; i++)
        {
            if (buffer[i] == null) break;

            float distance = Vector3.Distance(transform.position, buffer[i].transform.position); 
            if( distance < minDistance)
            {
                closestEnemy = buffer[i].GetComponent<Enemy>();
                minDistance = distance;
            }
        }
        return closestEnemy;
    }

    private void Update()
    {
        Profiler.BeginSample("가까운 적찾는 로직");
        Enemy target = GetClosestEnemy();
        Profiler.EndSample();
        if(target != null)
        {
            transform.LookAt(target.transform.position);
            target.TakeDamage(100);
        }
    }
    }

이번엔 코루틴으로 바꿔보자.
    private WaitForSeconds ws = new WaitForSeconds(0.2f);
    IEnumerator Attack()
    {
        while (true)
        {
            yield return ws;
            //공격로직
            Debug.Log("Attack");
        }
    }
코루틴도 캐싱처리하고 하면 최적화가 잘된다.