Preface: UnityEngine.Object
유니티에서의 모든 게임 오브젝트는 UnityEngine.Object로부터 상속을 받는다.
UnityEngine.Object는 유니티 엔진에서 생성된 C++ 내부 구현 객체와 C#에서 해당 객체의 포인터를 참조하는 인터페이스 클래스로 구성되어 있다.
여기서 C++은 unmanaged 언어라 프로그래머가 객체의 메모리 해제 시점을 결정할 수 있지만 C#은 managed 언어이기 때문에 메모리 해제를 GC에 맡겨야 하는 차이가 있다.
UnityEngine.Object의 파괴
유니티로 코딩을 하면서 UnityEngine.Object(혹은 그를 상속하는)를 파괴하려고 하면 Destroy() 또는 DestroyImmediate()를 호출하게 된다.
이때 위에 언급한 두 언어의 특성 차이로 인해 유니티 엔진에서는 C++ 구현 객체만 직접 해제하도록 하고 C# 인터페이스는 GC가 언젠간 해제한다는 믿음으로 방치하게 된다.
UnityEngine.Object의 null 확인
유니티에서의 모든 게임 오브젝트는 UnityEngine.Object로부터 상속을 받는다. 그리고 "일단은" 이러한 게임 오브젝트들의 파괴 여부를 확인할 수 있도록 되어있다.
MonoBehaviour를 상속하는 Box 클래스를 가진 BoxBox 오브젝트가 있을 때 null 확인을 해보겠다.
using UnityEngine;
public class TestNull : MonoBehaviour
{
Box m_Box;
void Start()
{
m_Box = FindObjectOfType<Box>();
Debug.Log(m_Box == null);
DestroyImmediate(m_Box);
Debug.Log(m_Box == null);
}
}
해당 클래스를 가진 오브젝트를 만들고 플레이 모드로 진입하면 아래와 같은 로그를 볼 수 있다.
이 코드는 적절히 큰 수만큼 실행해도 항상 같은 결과를 볼 수 있다. 위에서 배운 이론대로라면 C# 인터페이스는 당장 해제될 수 없으므로 아직 살아있는 상태일 수 있기 때문에 True가 나올 수 없을 것이다. 하지만 True가 나온다. 왜?
== 연산자 오버로딩을 통한 Fake Null
사실 유니티 엔진은 개발의 편의성을 위해 일단 Destroy를 하면 null이 될 수 있도록 처리를 했다. 만약 그렇지 않더라면 오브젝트의 파괴 여부를 확인하기 위해 더 귀찮은 과정이 생겼을 수도 있다. 그리고 이를 fake null이라고 부른다.
Fake null은 UnityEngine.Object에서 == 연산자를 오버로딩하여 구현한다.
추적을 통해 UnityEngine.Object의 코드를 살펴보면 == 와 != 를 오버로딩한 것을 볼 수 있다. 오버로딩된 ==,!= 연산자는 null과 비교할 때 다음 연산을 거치는 것으로 알려져 있다.
- C# 클래스가 해제되었는지 아닌지 살펴본다. 해제되어 있으면 null인 것이다.
- 만약 C# 클래스가 해제되지 않았다면 C++ 내부 구현 객체가 해제되어 있는지 살펴본다. 해당 객체가 해제되었으면 null이다.
이 구현으로 인해 실제 개발에서 몇몇 제약이 생긴다.
따라서 UnityEngine.Object의 null 확인은 상당히 무겁다.
기존과 비교해 한 단계 더 추가되었으므로 UnityEngine.Object의 null 확인은 무거운 작업이다. 따라서 가능하면 null 확인을 하지 않을 것을 권고하고 있다.
C#의 일부 문법을 사용할 수 없다.
UnityEngine.Object의 인터페이스를 둘러보면 알 수 있지만 is,?? 와 같은 연산자는 오버로딩을 하지 않았다. 따라서 ==,!= 외의 연산자로 null 확인을 제대로 할 수 없다는 의미가 된다. UnityEngine.Object를 상속하는 오브젝트에서는 가급적 ==,!=를 제외한 null 관련 연산자를 사용하면 안 된다. 일부 포스트에서는 is 연산자가 빠르다는 말이 있지만 당연히 오버로딩이 되지 않은 거라 쓸모가 없고 버그가 생길 요인이 된다.
Null 비교를 조금 더 빠르게 하는 법
하지만 실제 개발을 하다 보면 null 비교는 안 할 수가 없기 마련이다. 이를 극복하는 방법들이 인터넷에서 전해지고 있는데 이 글에서는 몇 가지를 소개한다.
OnDestroy()가 호출될 때 표시하고 is null과 같이 쓰기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Box : Observer
{
public bool IsNull
{
get => m_IsNull;
}
bool m_IsNull = true;
void Awake()
{
m_IsNull = false;
}
void OnDetroy()
{
m_IsNull = true;
}
}
// example: (obj is null || obj.IsNull)
아이디어는 Destroy 되었다는 표시를 멤버로 남기고 C# 클래스만 확인하는 is null과 같이 쓰면 C++ 객체까지 갈 볼 필요가 없다는 것이다. 하지만 OnDestroy()는 프레임이 끝난 후 호출되므로 Destroy 하는 프레임에서 null 체크를 같이 해야 한다면 이는 무용지물이다.
Destroy() 호출 후 명시적으로 null을 대입하고 is null 쓰기
Destroy(go)
go = null;
if (go is null)
{
Debug.Log("is null");
}
파괴를 명시적으로 할 수 있다면 이처럼 파괴 후 직접 null을 대입하면 is null을 활용할 수 있다.
결론
이번 포스트에서는 UnityEngine.Object의 구성을 간단히 알아보고 fake null과 그로부터 오는 성능 저하, 이를 극복하기 위한 트릭을 소개했다. Fake null의 성능 저하를 막는 방법은 특정한 상황으로만 한정되기 때문에 그 사용이 굉장히 어려울 수 있다. 하지만 적절한 규칙이 정해져 있거나 상황을 통제할 수 있으면 최적화에 도움이 될 것이다.
'Unity Engine > 기초 테크닉' 카테고리의 다른 글
[Unity] 오브젝트 클릭과 터치 감지 구현하기 (1) | 2024.11.18 |
---|---|
[Unity] 2D 오브젝트가 이동 중에 걸리는 현상 - Ghost Vertices (0) | 2024.11.10 |
[Unity] 스프라이트 시트로 랜덤 파티클 만들기 (2) | 2024.11.07 |
[Unity] 스프라이트 뭉치로 애니메이션 만들기 (1) | 2024.11.05 |
유니티 Raycast로 3D 오브젝트 클릭과 드래그 (2) | 2022.09.20 |