Content Directories 의 참조 카운트는 핸들만 센다
Loadable 로 프리팹을 올려 인스턴스를 만들고, 다 썼다 싶어 Release() 했는데 화면에서 오브젝트가 사라집니다. 에러도 경고도 없습니다. 반대로 인스턴스를 전부 지웠는데도 머티리얼이 메모리에 그대로 남아 있기도 합니다.
Content Directories 는 Loadable 핸들과 씬만 세고, 마지막 쪽이 놓은 1프레임 뒤에 에셋을 내립니다. Instantiate 로 만든 인스턴스는 이 셈에 들어가지 않습니다.
핸들 하나는 몇 번 Load 해도 하나로 센다
같은 Loadable 인스턴스에서 Load() 를 두 번 부르고, Release() 를 한 번씩 불렀습니다. 콘텐츠 머티리얼 수는 1프레임 뒤에 셌습니다.
| 단계 | 콘텐츠 머티리얼 | Status |
|---|---|---|
같은 핸들로 Load() 두 번 |
10 | Loaded, 두 결과가 같은 객체 |
Release() 한 번 |
9 | None |
Release() 한 번 더 |
9 | None, 에러 없음 |
Load() 를 몇 번 불렀든 Release() 한 번에 내려갑니다. 핸들 하나가 셈 하나입니다. 두 번째 Release() 는 아무 일도 하지 않습니다.
같은 에셋을 가리키는 Loadable 을 두 개 만들면 각자 하나씩 셉니다.
| 단계 | 콘텐츠 머티리얼 | 원본 / 두 번째 |
|---|---|---|
두 핸들 Load() |
10 | Loaded / Loaded, 같은 객체 |
원본만 Release() |
10 | None / Loaded |
두 번째도 Release() |
9 | None / None |
두 핸들이 돌려준 것은 같은 객체이고, 마지막 핸들을 놓을 때 내려갑니다. 그리고 내려가는 시점은 Release() 를 부른 프레임이 아니라 그다음 프레임이었습니다. Release() 직후에 세면 아직 10 입니다. 에디터와 Player 가 같았습니다.
C++ 의 shared_ptr 처럼 손잡이 하나가 강한 참조 하나라는 점은 같지만, 복사하거나 사라질 때 저절로 늘고 주는 방식이 아닙니다. 부르는 쪽이 Load() 와 Release() 로 직접 올리고 내립니다.
같이 쓰는 에셋은 마지막 사용자가 놓을 때 내려간다
Cube 의 머티리얼과 InDoor 씬의 머티리얼 여섯 개는 같은 Lit.shader 를 씁니다. Cube 핸들과 씬을 둘 다 올린 뒤 내리는 순서를 바꿔 봤습니다.
| 내리는 순서 | 첫 번째를 내린 뒤 셰이더 | 두 번째를 내린 뒤 셰이더 |
|---|---|---|
Cube Release() → 씬 언로드 |
남음 | 3개 내려감 |
씬 언로드 → Cube Release() |
남음 (다섯 번 모두) | 3개 내려감 |
어느 순서든 마지막으로 쓰던 쪽이 놓을 때 내려갑니다. 그리고 Resources.UnloadUnusedAssets() 를 부르지 않아도 내려갑니다. 놓은 에셋만 처리하고 전체 오브젝트를 훑지 않습니다.
거꾸로 핸들이 잡고 있는 에셋은 UnloadUnusedAssets() 를 불러도 안 내려갑니다. Cube 핸들만 잡고 C# 변수도 인스턴스도 없는 상태에서 불렀을 때 Unloading 0 unused Assets 가 찍히고 Cube 는 Loaded 로 남았습니다.
| 셈을 움직이는 것 | 셈을 안 움직이는 것 |
|---|---|
Loadable.Load() / Release() |
Instantiate / Destroy |
| 씬 로드 / 언로드 | renderer.sharedMaterial = … 같은 대입 |
| 디렉터리 등록 / 해제 (Root) | C# 변수로 들고 있는 것 |
Instantiate 한 인스턴스는 세지 않는다
표의 오른쪽 칸이 사고가 나는 자리입니다. Cube 를 Load() 해서 Instantiate 한 뒤, 인스턴스는 그대로 두고 핸들만 Release() 했습니다.
| 시점 | 원본 프리팹 | 씬의 인스턴스 | 인스턴스의 머티리얼 |
|---|---|---|---|
Release() 한 프레임 |
정상 | 정상 | Lit |
| 1프레임 뒤 | 파괴됨 (== null 이 참) |
살아 있음 | null |
인스턴스는 씬에 남아 있는데, 그 인스턴스가 쓰던 머티리얼은 셈이 0 이 되어 내려갔습니다. 경고는 없습니다. 화면에서는 어떻게 보이는지 Player 에서 큐브 가운데 픽셀을 찍었습니다.
데이터 상태는 같고 그리는 방식만 다릅니다. URP 의 GPU Resident Drawer 가 켜져 있으면 머티리얼이 빈 렌더러를 그리기에서 빼는 것으로 보이는데, 내부 이유는 확인하지 못했습니다. 어느 쪽이든 핸들을 놓은 쪽 코드에서는 아무 신호가 없습니다.
해제된 원본으로 다시 Instantiate 하면 에러가 나는데, 에디터와 Player 의 메시지가 다릅니다. 에디터는 MissingReferenceException 으로 “이미 파괴됐다” 고 알려 주지만, Player 는 ArgumentNullException: Value cannot be null. Parameter name: data 라서 원인을 떠올리기 어렵습니다.
직접 확인하는 테스트 코드
E1 은 인스턴스를 두고 핸들만 놓고, E2 는 같은 에셋에 핸들 둘을 만들어 하나씩 놓습니다. 결과는 1프레임마다 찍어 내려가는 프레임을 확인합니다.
ContentPackRootAsset m_Root;
GameObject m_Instance;
Loadable<GameObject> m_Second; // 두 번째 핸들은 필드로 들고 있어야 한다
// E1: 인스턴스는 둔 채 핸들만 놓는다
IEnumerator ReleaseWhileInUse()
{
m_Instance = Instantiate(m_Root.bonusPrefab.Load());
yield return null;
m_Root.bonusPrefab.Release();
for (int i = 0; i <= 2; i++)
{
var r = m_Instance.GetComponent<Renderer>();
Debug.Log($"+{i}프레임 인스턴스 살아있음:{(bool)m_Instance} 머티리얼 null:{r.sharedMaterial == null}");
yield return null;
}
}
// E2: 같은 에셋, 다른 핸들 둘
IEnumerator TwoHandles()
{
m_Second = new Loadable<GameObject>(m_Root.bonusPrefab.LoadableObjectId);
GameObject a = m_Root.bonusPrefab.Load();
GameObject b = m_Second.Load();
Debug.Log($"같은 객체? {ReferenceEquals(a, b)}");
m_Root.bonusPrefab.Release();
yield return null;
Debug.Log($"원본만 놓은 뒤: {m_Root.bonusPrefab.Status} / {m_Second.Status}, 살아있음:{a != null}");
m_Second.Release();
yield return null;
Debug.Log($"둘 다 놓은 뒤 살아있음:{a != null}");
}
E1 에서 1프레임 뒤 “머티리얼 null:True” 가 나오고, E2 에서 원본만 놓은 뒤에는 살아 있다가 둘 다 놓은 뒤 false 가 되면 같은 규칙입니다.
주의할 점
1. 쓰는 곳마다 자기 Loadable 을 가진다
안 지키면 생기는 문제. Loadable 하나를 여러 시스템이 같이 쓰면, 몇 번 Load() 했든 셈은 하나입니다. 한 곳이 Release() 하면 다른 곳이 쓰던 에셋까지 내려갑니다.
이렇게 쓴다. 프리팹을 들고 있는 풀이나 스포너는 new Loadable<T>(원본.LoadableObjectId) 로 자기 핸들을 만들어 직접 올리고 내립니다.
2. 인스턴스를 다 지운 뒤에 Release 한다
안 지키면 생기는 문제. 인스턴스는 셈에 안 들어가서, 인스턴스가 남아 있어도 핸들을 놓으면 머티리얼이 내려갑니다. 화면에서 사라지거나 마젠타가 되고 경고는 없습니다.
이렇게 쓴다. 핸들의 수명을 그 프리팹으로 만든 인스턴스보다 길게 잡습니다. 인스턴스를 만든 컴포넌트의 OnDestroy 에서 인스턴스를 정리한 뒤 Release() 합니다.
3. 놓은 직후에는 아직 안 내려갔다
안 지키면 생기는 문제. Release() 나 씬 언로드 완료 이벤트 직후에 메모리를 재면 아직 그대로라 해제가 안 된 것처럼 보입니다.
이렇게 쓴다. 해제를 확인하거나 이어지는 작업을 할 때는 최소 1프레임을 기다립니다. 해제 완료를 알려 주는 이벤트나 셈을 읽는 API 는 찾지 못했습니다.
정리
Content Directories 는 Loadable 핸들과 씬만 세어, 마지막 쪽이 놓은 1프레임 뒤에 에셋을 내립니다.
- 핸들 하나는 몇 번
Load()해도 셈 하나이고, 같은 에셋에 핸들이 여럿이면 각자 하나씩 셉니다. 같이 쓰는 에셋은 마지막 사용자가 놓을 때 내려갑니다. UnloadUnusedAssets()없이 놓는 순간 그 에셋만 처리하고, 핸들이 잡은 에셋은UnloadUnusedAssets()도 안 내립니다.Instantiate한 인스턴스는 세지 않아서, 쓰는 중에 핸들을 놓으면 머티리얼이 경고 없이 빠집니다. 인스턴스를 다 지운 뒤 놓습니다.