유니티에서 스크립트는 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행된다.

이때 이 이벤트 함수들은 MonoBehaviour에 속해 있는 이벤트들이다.

 

스크립트는 에디터상에서 존재하는 오브젝트에 하나의 컴포넌트로 추가하여 프레임워크를 실행할 수 있는데,

여기서 말하는 프레임워크가 유니티 생명 주기(Unity Life Cycle)가 된다.

MonoBehaviour란?

유니티에서 게임 오브젝트를 제어하기 위해서는 MonoBehaviour라는 클래스를 상속한 스크립트의 컴포넌트를 추가해야만 한다.

MonoBehaviour는 유니티에서 제공되는 기본 클래스로써,

모든 스크립트는 처음 생성 시 default로 Monobehaviour에 파생되어 있다.

그렇기 때문에 여러 이벤트 함수들을 정의하여 사용할 수 있다.


생명 주기 실행 순서

라이프사이클 플로우차트(LifeCycle FlowChart)

https://docs.unity3d.com/kr/2019.4/Manual/ExecutionOrder.html#FirstSceneLoad

플로우 차트에 나와있는 이벤트들의 대부분은 MonoBehaviour를 통해 이벤트 발생 시의 로직을 추가할 수 있고,

그 외의 'Internal animation update'에 속해 있는 이벤트와 같은 경우 StateMachineBehaviour를 통해 추가가 가능하다.

 

플로우 차트중 회색 바탕으로 이루어진 이벤트들의 경우는 애니메이션을 처리할 때 호출되는 내부 함수이며,

직접 호출할 수 없는 이벤트이다.

 

또한 위에서 아래로 흐르면서 화살표로 표시된 부분들이 있다.

화살표로 표시된 부분 밑의 이벤트부터 그 다음 화살표로 표시된 부분 위의 이벤트까지 각 오브젝트별로 호출된다.

예를 들어 A와 B의 객체가 있다면 다음과 같은 호출을 가진다.

  • A의 Awake, OnEnable 호출
  • B의 Awake, OnEnable 호출
  • A의 Reset 호출
  • B의 Reset 호출
  • A의 Start 호출
  • B의 Start 호출
  • ...

초기화 (Initialization)

유니티 생명 주기에서의 초기화는 3단계에 걸쳐서 이루어지며, 프로그램 시작 시 시작 프레임에서 한 번만 호출된다.

  • Awake : 인스턴스화 된 직후에 호출
  • OnEnable : 오브젝트 활성화 직후 호출
  • Start : 첫 번째 프레임 업데이트 전에 호출

이 3가지의 초기화 함수는 게임 오브젝트가 시작하는 동안 비활성화 상태인 경우 호출되지 않는다.

에디터 (Editor)

  • Reset : 오브젝트에 컴포넌트를 처음 연결하거나, Reset 커맨드를 사용할 때 호출된다.

업데이트 (Update)

프로그램이 시작되고 매 프레임마다 혹은 주기적으로 계속 호출되는 이벤트들이다.

  • FixedUpdate : 신뢰할 수 있는 타이머를 통해 일정 시간마다 주기적으로 호출
  •  Update : 한 프레임당 한 번 호출
  • LateUpdate : Update가 끝난 이후, 한 프레임당 한 번 호출

FixedUpdate는 다른 Update들과 다르게 일정 시간을 주기로 호출되기 때문에,

프레임 속도에 따라 한 프레임당 여러 번 호출 혹은 한 프레임 사이에 호출되지 않을 수 있다.

물리 (Physics)

OnTrigger, OnCollision 등은 물리적 충돌과 관련 있는 함수들로

Enter, Stay, Exit로 나뉘며 기본적으로 FixedUpdate와 같은 주기로 충돌 검사와 호출이 이루어진다.

입력 이벤트 (Input Events)

OnMouseEnter, Exit, Over, Down 등의 이벤트들은 마우스와 오브젝트가 상호작용하는 함수들로

Update 함수와 비슷하게 매 프레임마다 상호작용을 체크하여 그에 대응하는 함수를 호출한다.

애니메이션 (Animation)

플로우차트에서 애니메이션 업데이트는 Physics와 Game Logic 두 부분에 나타나 있는 것을 알 수 있다.

그 이유는 프레임에 따른 업데이트와 물리 연산이 필요하여 일정 시간에 따른 업데이트로 나누기 때문이다.

  • OnStateMachineEnter/Exit : State Machine Update 단계 동안 컨트롤러의 상태 머신이 엔트리/종료 상태를 전환할 때 호출
  • Fire Animation Events : 애니메이션 클립에 있는 모든 애니메이션 이벤트들을 호출
  • StateMachineBehaviour callbacks : OnStateEnter/Update/Exit, 최대 3개의 활성 상태를 가지며, 각 조건이 만족될 때 정의된 콜백을 호출
  • OnAnimatorMove : Update 프레임마다 Animator 정보가 갱신될 시에 호출
  • OnStateMove : OnAnimatorMove가 호출된 직후 호출
  • OnAnimatorIK : Update 프레임마다 IK 정보가 갱신될 시에 호출
  • OnStateIK : OnAnimatorIK가 호출된 직후 호출

여기까진 오버라이딩을 통해 정의가 가능한 이벤트들이며,

이후는 따로 함수를 호출할 수 없고 Unity가 애니메이션을 처리할 때 호출되는 내부 함수이다.

  • WriteProperties : 애니메이션을 통해 변경된 프로퍼티를 씬에 적용할 때 호출
  • State Machine Update : 애니메이터 모드에 따라 주기적으로 호출
  • ProcessGraph : 모든 애니메이션 그래프를 평가할 때 호출
  • ProcessAnimation : 애니메이션 그래프의 결과를 블렌딩할 때 호출
  • WriteTransforms : 모든 애니메이션화된 트랜스폼을 워커 스레드에서 씬에 작성할 때 호출

상태 머신 평가는 보통 멀티 스레드이지만, 특정 콜백(OnStateMachineEnter, OnStateMachineExit)을 추가하면 멀티스레딩이 비활성화된다.

렌더링 (Rendering)

LateUpdate가 끝난 이후에 호출되는 함수들이다.

  • OnPreCull : 카메라가 씬을 컬링하기 전에 호출
  • OnBecameVisible/Invisible : 오브젝트가 카메라에 표시되거나/되지 않을 때 호출
  • OnWillRenderObject : 오브젝트가 표시되면 각 카메라에 한 번 호출
  • OnPreRender : 카메라가 씬 렌더링을 시작하기 전에 호출
  • OnRenderObject : 모든 일반 씬 렌더링이 처리된 후 호출
  • OnRenderImage : 씬 레더링이 완료된 후 호출
  • OnGUI : GUI 이벤트에 따라 프레임당 여러 번 호출
  • OnDrawGizmos : 시각화 목적을 위한 기즈모를 그릴 때 호출

코루틴 (Coroutine)

일반적으로 Update 함수가 반환된 이후에 실행되며,

YieldInstruction이 완료될 때까지 다수의 프레임으로 나누어 호출된다.

자세한 설명은 아래 포스팅을 참고하자.

https://lms0408.tistory.com/1

 

[Unity] Coroutine

Unity에서 Coroutine은 단일 프레임 내에서 작업을 수행하지 않고 다수 프레임에서 작업을 수행할 수 있다.그로 인해 비동기적 처리 방식을 위한 코드를 구현할 때는 Coroutine을 많이 사용하기도 한다

lms0408.tistory.com

  • yield WaitForSeconds : 지정한 시간이 지난 후, 모든 Update 함수가 프레임에 호출된 후 계속해서 호출
  • yield WaitForFixedUpdate : 모든 FixedUpdate가 모든 스크립트에 호출된 후 계속해서 호출
  • yield WWW : www 다운로드가 완료된 후 계속해서 호출
  • yield StartCoroutine : 해당 코루틴이 완료되기까지 기다렸다 계속해서 호출
  • yield WaitForEndOfFrame : 한 프레임에서 호출되는 모든 이벤트가 끝난 이후 계속해서 호출

일시 정지 (Pausing)

  • OnApplicationPause : 프로그램이 일시정지 혹은 재개 될 때 호출

해제 (Decommissioning)

  • OnApplicationQuit : 프로그램이 종료될 때 한번 호출
  • OnDisable : 오브젝트가 비활성화 혹은 씬이 종료될 때 한번 호출
  • OnDestroy : 씬이 종료될 때 한번 호출

반복 구간 (Loop)

유니티 생명 주기 플로우 차트를 보면 부분적으로 반복을 하고있다고 표시한 부분들이 있다.

ex) (FixedUpdate ~ yieldWaitForFixedUpdate), (OnGUI) 등

기본적으로 FixedUpdate부터 OnApplicationQuit까지 한 프레임을 기준으로 이루어진 생명 주기이다.

그러나 한 프레임에 여러 번 혹은 한 번도 호출되지 않는 이벤트의 경우 따로 Loop의 형태로 표시한다.


마무리

코드를 작성하다보면 객체의 초기화 순서에 차이로 오류가 발생하는 경우가 발생한다.

코드를 작성하고 그 흐름을 알기 위해선 생명 주기에 대한 이해는 필수이다.


참고 문서 : https://docs.unity3d.com/kr/2019.4/Manual/ExecutionOrder.html#FirstSceneLoad

                  :  https://docs.unity3d.com/kr/2020.3/Manual/class-MonoBehaviour.html

개발을 할 때 메모리 관리는 필수 요소이다.

메모리를 관리하지 않는다면 메모리 누수로 인해 예기치 못한 상황이 발생하게 될 것이다.

 

Unity에서는 C# .NET Framework의 기반인 만큼 가비지 컬렉터가 존재한다.(C# 가비지 컬렉터)

하지만 Unity의 가비지 컬렉터는  .NET Framework에서 지원하는 가비지 컬렉터와는 조금 다른데,

그 부분과 유니티의 Managed Heap은 어떤 식으로 관리되는지 알아보려고 한다.


유니티 가비지 컬렉터 작동 방식

유니티는 총 두가지의 방식으로 가비지 메모리를 수집할 수 있다.

보엠 가비지 컬렉션(Boehm Garbage Collection)

기본적으로 유니티 가비지 컬렉터는 Boehm GC를 사용하고 있다.

Boehm GC는 Mark and Sweep 알고리즘을 사용한다.

Boehm GC는 Stop-the-World 가비지 콜렉터이므로 유니티 가비지 컬렉션이 시작될 땐 프로그램 전체가 멈춘다.

 

즉 정리해야할 메모리가 많아진다면 그만큼 대기해야하는 시간이 길어지므로 가비지 컬렉터 호출을 최소화 해야한다.

점진적 가비지 컬렉션(Incremental Garbage Collection)

유니티는 2019.1에 점진적 가비지 컬렉션을 추가했다.

기존 Boehm GC같은 경우 가비지 컬렉션 수행 되는 동안 프로그램이 멈추게 되지만,

점진적 가비지 컬렉션을 사용하게 될 경우 작업을 다수 프레임으로 분산시켜 프로그램 실행을 보다 더 짧게 중단시킨다.

 

점진적 가비지 컬렉션을 활성화 하는 방법은 다음과 같다.( Edit > ProjectSettings > Player > Other Settings > Configuration > Use Incremental GC )

 

기본적으로 Mark and Sweep 방식을 통해 메모리를 관리하지만 Compaction 과정을 거치지 않기 때문에 메모리 누수 현상은 피해갈 수 없는 문제이다.


Unity Managed Heap

.NET Framework의 가비지 컬렉터는 세대별 가비지 컬렉션( Generation Garbage Collection)으로,

세대별로 관리되는 힙을 나누어 메모리를 관리한다.

 

https://docs.unity3d.com/kr/2021.2/Manual/performance-managed-memory.html

반면 Unity의 힙은 세대별, SOH와 LOH 등으로 힙을 따로 나누지 않고 모든 객체들을 한 곳에서 관리한다.

https://docs.unity3d.com/kr/2021.2/Manual/performance-managed-memory.html

또한 Compaction 과정을 하지 않으므로 위의 그림과 같이 메모리 세그먼트 사이에 "간극"이 생기게 되는데,

이 간극에는 해당 공간의 용량보다 작거나 같은 데이터밖에 저장할 수 없게 되어 메모리 누수가 발생하게 된다.

https://docs.unity3d.com/kr/2021.2/Manual/performance-managed-memory.html

위와 같이 int형 배열을 메모리에 할당하려고 할 때 전체적으로 할당 가능한 충분한 메모리임에도 간극으로 인해 메모리를 할당하지 못할경우 유니티 메모리 관리자는 어떻게 할까?

 

유니티 메모리 관리자는 위와 같은 상황에 처했을 때 다음과 같은 작업을 실행한다.

  • 우선, 가비지 컬렉터가 아직 실행되지 않은 경우 이를 실행시키고, 수용 가능한 충분한 공간을 만들 수 있도록 시도한다.
  • 가비지 컬렉터가 실행된 이후에도 수용가능한 충분한 공간이 없는 경우에는 힙을 확장시킨다. 이때 힙이 확장되는 구체적인 양은 플랫폼에 따라 다르지만, 대부분의 플랫폼에서는 힙이 확장될 때 이전 확장의 두 배만큼 확장된다.

이렇게 늘어난 힙 공간은 더 큰 할당이 발생하는 경우 힙을 재확장해야 할 필요가 없도록 하기 위해 많은 빈 공간이 존재하여도 그대로 유지한다.

하지만 이러한 방식은 결국 메모리를 무지하게 잡아먹는 원인이 되고,더 이상 확장할 메모리가 없게 된다면 예기치 못한 상황이 발생하게 될 것이다.


마무리

무분별한 메모리 할당은 잦은 가비지 컬렉터 호출로 이어지며,

더 큰 힙 메모리로 확장하게 되면서 시스템의 많은 메모리를 차지하게 될 것이다.

이러한 문제는 곧 성능 저하로 이어지며 사용자에게 좋지 않은 경험을 제공하기 때문에 더더욱 최적화에 신경을 써야한다.

 

예를 들어 재사용성이 높은 객체의 경우 한 번 사용한 이후 Destory 메서드를 호출하여 객체를 파괴하는 방식보단,

ObjectPooling을 이용하여 객체를 미리 할당한 이후 OnEnable과 OnDisable을 통해 객체를 재사용 하는 방식으로 메모리 누수를 방지하거나,

 

문자열 연산을 반복할 경우 StringBuilder 클래스를 통해 객체를 생성하지 않고 객체를 변형하는 방식을 사용하는 것이 좋다.

 

또한 유니티 프로파일러를 통해 메모리를 실시간 확인하며 관리하는 것도 좋은 방법이다.


참고 문서 : https://docs.unity3d.com/kr/2021.2/Manual/performance-managed-memory.html

 

 

Unity에서 Coroutine은 단일 프레임 내에서 작업을 수행하지 않고 다수 프레임에서 작업을 수행할 수 있다.

그로 인해 비동기적 처리 방식을 위한 코드를 구현할 때는 Coroutine을 많이 사용하기도 한다.

하지만 Coroutine의 이점만 바라보고 사용하기에는 불이점도 분명히 존재하기 때문에, Coroutine에 대해 더 자세히 알아보고 정리하자는 마음으로 이 글을 작성하게 되었다.

 


Coroutine은 비동기처리 방식?

비동기에 대한 이해가 부족하다면 아래의 글을 보고오자.

 

Synchronous vs Asynchronous & Blocking vs Non-blocking

Synchronous와 Asynchronous, Blocking과 Non-blocking이라는 말은 운영체제(OS)를 공부하면서 듣게 된 개념이다.프로그래밍을 함에 있어 중요한 개념인 만큼 이번 기회에 정리를 해보고자 한다.Synchronous와 Asyn

lms0408.tistory.com

 

 

Unity Documentation에 코루틴에 대한 설명을 보면 다음과 같다.

※ 코루틴은 HTTP 전송, 에셋 로드, 파일 I/O 완료 등을 기다리는 것과 같이 긴 비동기 작업을 처리해야 하는 경우 코루틴을 사용하는 것이 가장 좋습니다.

- https://docs.unity3d.com/kr/2022.3/Manual/Coroutines.html

 

코루틴 - Unity 매뉴얼

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있습니다. Unity에서 코루틴은 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드입니

docs.unity3d.com

유니티의 Coroutine은 작업을 다수의 프레임에 분산하여 수행할 수 있는 Class이다.

즉, 작업이 완료되지 않더라도 다른 작업을 수행할 수 있다는 뜻이다.

그렇기 때문에 유니티의 Coroutine은 비동기 방식이다.


Coroutine 동작 원리

Unity의 Coroutine은 반환 키워드 yield와 Coroutine의 반환 타입인 IEnumerator를 통해 비동기 작업을 수행한다.

 

'yield'란 양도하다는 의미로 Unity에서 Coroutine을 사용할 때의 yield 키워드는 함수의 실행을 일시 중지하고, 호출자에게 제어를 반환한다는 의미라고 볼 수 있다.

또한 yield 키워드 뒤에 오는 표현식에 따라 Coroutine이 재개될 조건을 지정할 수 있다.

public interface IEnumerator
{
    object Current { get; }
    bool MoveNext();
    void Reset();
}

IEnumerator(열거자)는 컬렉션을 단순하게 반복할 수 있도록 지원해주며, System.Collections 네임스페이스에 속한 interface이다.

IEnumerator는 현재 위치의 데이터를 반환해주는 Object Property와 다음 데이터로이동 가능에 대한 반환과 이동을 시켜주는 Bool형 메서드, 인덱스를 초기 상태 위치로 변경해주는 Void형 메서드로 이루어져있다.

 

다수 프레임에서 작업을 나누어 수행할 수 있는 이유는

yield return 문이 컴파일시 코드의 위치를 저장하는 상태 머신으로 변환되어

yield를 기준으로 작업을 나누어 수행할 수 있기 때문이다.

 

코루틴을 실행하는 메서드는 기본적으로 Enumerator에 함수의 포인터 값으로 저장된다.코루틴 실행 시 반복문을 만나면서 MoveNext()가 호출된다.이때 IEnumerator 반환형 메서드가 yield문을 만나기 전 까지 실행이 된다.

 

yield문을 만나게 되면 Current인 YieldInstruction(waitForSeconds와 같은)이 실행되게 된다.이때 리턴되는 값이 존재한다면 MoveNext()의 결과값은 true로 반환되고,없을 시 false를 반환하여 반복문이 종료된다.

 

MoveNext()의 반환 값이 true가 되어 Current를 실행하였다면,다시 MoveNext()가 호출되면서 yield return이 일어났던 위치의 다음줄 부터 재실행이 된다.이때 역시 마찬가지로 yield 문을 만나기 이전까지 진행된다.

 

위의 과정을 반복하여 최종적으로 MoveNext()의 반환 값이 false가 반환되면 반복문이 종료되면서 코루틴 실행이 끝이 나게 된다.


Coroutine 사용법

Coroutine을 사용하기 위해서는 첫 번째로 IEnumerator 반환형으로 메서드를 정의해주어야 한다.

이때 반환형은 한 개 이상의 yield 키워드를 포함한 return 문이 작성되어야 한다.

IEnumerator CoroutineTest()
{
    // ...Something
    yield return ...
}

이렇게 정의된 메서드는 MonoBehaviour에 상속된 인스턴스에서 StartCoroutine() 메서드를 통해 실행시킬 수 있다.

public class CoroutineControl : MonoBehaviour
{
    // ... CoroutineTest Method
    void Start()
    {
    	StartCoroutine(CoroutineTest());
    }
}

StartCoroutine을 통해 생성된 Coroutine을 field 변수에 저장하여 Coroutine을 관리할 수 있다.

public class CoroutineControl : MonoBehaviour
{
    // ... CoroutineTest Method
    Coroutine coroutine;
    void Start()
    {
    	coroutine = StartCoroutine(CoroutineTest());
        if ( // ... Any Condition )
        {
            StopCoroutine(coroutine);
        }
    }
}

이렇게 정지된 Coroutine은 어떠한 곳에도 참조가 되어있지 않을 시 GC(Garbage Collector)에 의해 수집되게 된다.

yield

Coroutine을 사용할 때 대표적으로 쓰는 반환형들은 다음과 같다.

yield return null 다음 프레임까지 대기
yield return new WaitForSeconds() 입력한 초(sec)만큼 대기
yield return new WaitFixedUpdate() 다음 프레임의 FixedUpdate까지 대기
yield return new WaitForEndOfFrame() 모든 랜더링 작업이 끝날 때까지 대기
yield return new StartCoroutine() 입력한 다른 코루틴이 끝날 때까지 대기
yield return new WWW() 입력한 웹 통신 작업이 끝날 때까지 대기
yield return new AsyncIoeration 비동기 작업이 끝날때까지 대기
yield break 코루틴 종료

Coroutine의 문제점

Coroutine은  비동기적 방식과 유사한 방식으로 작업을 편리하게 수행할 수 있는 만큼 남용할 시 문제가 생길 수 있다.

 

다음 예는 게임 내에서 Monster의 공격이 Coroutine으로 구현되어있는 상황에서 메모리를 낭비하는 모습이다.

public class Monster : MonoBehaviour
{
    public IEnumurator DashAttack()
    {
    	float waitAttackTime = 1f;
    	yield return new WaitForSeconds(waitAttackTime);
        // ...Attack
        yield break;
    }
}

Monster의 수가 비약적으로 적은 경우에는 위의 코드를 사용했을 때 큰 문제는 발생하지 않을 것이다. (다만 Coroutine을 생성하여 공격한다는 점에서 메모리 손실이 발생한다.)

 

하지만 Monster의 수가 많아진다면 최대 n개(n개의 몬스터가 동시에 공격했을 때) 만큼 Coroutine의 인스턴스가 생성될 것이다.

 

더 최악의 상황은 이렇게 생성된 Coroutine이 재사용 가능성이 없게 된다면 사용한 Coroutine n개가 GC에 의해 수집이 되어 더 잦은 GC 호출을 초래할 수 있는 문제가 생긴다.

 

이렇게 잦은 GC 호출은 높은 CPU점유율로 인한 프레임 드랍 현상까지 이어지게 된다.

 

+ Recent posts