본문 바로가기
Unity/어드레서블 에셋 시스템

어드레서블 에셋 시스템 - 개념: 에셋 로드와 생성 및 해제

by PlaneK 2020. 6. 24.

 에셋 로드와 생성 및 해제

프리팹 에셋을 로드하고 게임오브젝트로 생성해보자. 그리고 에셋을 메모리에서 할당 해제해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void Start()
{
    // 비동기적으로 에셋을 로드한다. 
    // 로컬 저장소라도 로드하는데 프레임이 소요된다.
    assetReferenceGameObject.LoadAssetAsync();
    assetReferenceMaterial.LoadAssetAsync();
}
 
void Update()
{
    // IsDone 플래그는 로드가 완료되면 True를 반환한다.
    if (assetReferenceGameObject.IsDone)
        // 어드레서블의 Instantiate가 아닌 기존 GameObject의 Instantiate을 사용한 것에 유의하자.
        var go = GameObject.Instantiate(assetReferenceGameObject.Asset, transform) as GameObject;
 
    // IsDone 필드 대신 Asset 필드의 null 체크로도 로직을 구성할 수 있다.
    if (assetReferenceMaterial.Asset != null)
        go.GetComponent<MeshRenderer>.material = assetReferenceMaterial.Asset as Material;
}
 
private void OnDisable()
{
    // 메모리에 올라간 에셋들을 해제해 준다.
    // 해당 에셋을 사용하는 개체가 존재하는지부터 체크하고 할당 해제해야 한다.
    assetReferenceGameObject.ReleaseAsset();
    assetReferenceMaterial.ReleaseAsset();
}
 

샘플 코드는 위와 같다.

머티리얼 에셋을 사용중인 개체가 존재하는데 불구하고 Release를 호출하면 우측과 같은 상황이 발생한다.

따라서 Release하기 전에 Ref Count를 유의하는 것이 안전하다.

"만약 Addressable의 InstantiateAsync함수로 생성했다면 Release 시 Ref Count가 0이 될때까지 해제하지 않고 기다려준다."

Cube는 왜 메모리에서 사라지지 않지?

위 Cube는 로드한 Prefab을 Instantiate한 결과물이다. 

Prefab을 Release했다고 해서 Cube가 Destroy되지 않는다.

프로파일러를 통해 메모리 상태를 보자.

Prefab은 Asset영역에 위치하고 Cube(Clone)은 Scene Memory 영역에 위치한다.

즉, Asset이 아니다. Release 함수로 Asset은 해제됐지만 각 Cube(Clone)들은 별도로 Destroy해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using UnityEngine.AddressableAssets;
 
public class SelfDestruct : MonoBehaviour {
 
    public float lifetime = 5f;
 
    void Start()
    {
        Invoke("Release", lifetime);
    }
 
    void Release()
    {
        // 어드레서블의 Release 함수를 실행하고 Destroy되지 않으면
        if (!Addressables.ReleaseInstance(gameObject))
            // Destroy 실행
            Destroy(gameObject);
    }
}
 

Cube(Clone)을 메모리에서 Destroy하려면 ReleaseInstance나 Destroy를 호출해야하는데

위 예시에서는 GameObject.Instantiate를 사용했으므로 Destroy를 사용해 메모리해제한다.

에러 디버깅
UnityEngine.ResourceManagement.ResourceManager+CompletedOperation`1[UnityEngine.Material], result='', status='Failed': Exception of type 'UnityEngine.AddressableAssets.InvalidKeyException' was thrown., Key=503c0cd611e6fe648afc5821a7540b3b, Type=UnityEngine.Material 

로드가 실패하면서 위와 같이 InvalidKeyException이 발생했다.
해당 에러는 Catalog에 매칭되는 Key가 없기 때문에 발생한 것이다.
따라서 에셋그룹을 다시 빌드해서 Catalog를 업데이트하면 해결된다

 

어드레서블로 생성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
 
public class FilteredReferences : MonoBehaviour
{
    [Serializable]
    public class AssetReferenceMaterial : AssetReferenceT<Material>
    {
        public AssetReferenceMaterial(string guid) : base(guid) { }
    }
 
    public AssetReferenceGameObject assetReferenceGameObject;
    public AssetReferenceMaterial assetReferenceMaterial;
 
    private GameObject m_go;
 
    public void OnClickLoadAssetAsync()
    {
        // 로드와 동시에 콜백을 등록하자.
        // 어드레서블은 Completed를 통해 콜백 등록과 동시에 로드를 진행한다.
        assetReferenceGameObject.LoadAssetAsync().Completed += OnLoadGameObject;
    }
 
    private void OnLoadGameObject(AsyncOperationHandle<GameObject> asyncOperationHandle)
    {
        Debug.Log("Load is Done 'GameObject'");
        assetReferenceMaterial.LoadAssetAsync().Completed += OnLoadMaterial;
    }
 
    private void OnLoadMaterial(AsyncOperationHandle<Material> asyncOperationHandle)
    {
        Debug.Log("Load is Done 'Material'");
 
        // GameObject가 아닌 어드레서블을 통해 생성해보자.
        var go = assetReferenceGameObject.InstantiateAsync(transform.position, Quaternion.identity);
        // 생성된 개체의 GameObject는 Result 필드가 반환한다.
        go.Result.GetComponent<MeshRenderer>().material = assetReferenceMaterial.Asset as Material;
 
        m_go = go.Result;
    }
 
    public void OnClickRelease()
    {
        assetReferenceGameObject.ReleaseAsset();
        assetReferenceMaterial.ReleaseAsset();
    }
 
    public void OnClickDestroy()
    {
        if (!Addressables.ReleaseInstance(m_go))
            Destroy(m_go);
    }
}

똑같이 어드레서블로 Instantiate 하고나서 Cube가 Destroy 되기전에 Release를 해봤다.

GameObject로 생성된 것과 다르게 Material이 Release되지 않았다.

프로파일러로 Test 머티리얼의 Ref Count를 살펴보자.

Release를 호출해도 에셋이 메모리에서 할당 해제되지 않고 Cube(Clone)가 계속 사용하고 있다.

Cube(Clone)을 Destroy하고나니 Test 머티리얼이 해제됐다.

이렇다는 의미는 GameObject.Instantiate 대신 Addressables의 Instantiate을 사용하는 것이 훨씬 안전하고 편하다고 할 수 있다.

Release 후 Instantiate을 해보았다.

Release 후 Scene Memory에 Instnatiate으로 생성된 오브젝트 외 다른 오브젝트들은 Release 함수에 의해 에셋에 대한 링크가 끊어졌다.

따라서 해당 에셋이 null을 반환하기 때문에 Instantiate를 하면 위와같이 된다.

반면 SceneMemory에 생성된 Cube들은 Destroy되기 전까지 안전하게 에셋 참조를 유지하고 있다.

"근데 AssetReference.Asset이 null인데 어떻게 Cube를 Instantiate하고 있지?"

머티리얼의 경우 Cube 외 아무도 참조하고 있지 않다.

그러나 GameObject(프리팹)의 경우는 MonoBehaviourCallbackHooks라는 MonoBehaviour가 참조하고 있는데

아마 SceneMemory의 FiteredReferences일 것이다. AssetReference의 'Asset'필드는 Null이더라도 AssetReference의 Instantiate함수를 통해서는 에셋을 참조할 것이다. 그래서 Asset이 null이라도 생성이 된 것이다.

1
2
3
4
5
6
7
if (assetReferenceGameObject.Asset != null)
{
    var go = assetReferenceGameObject.InstantiateAsync(transform.position, Quaternion.identity);
    go.Result.GetComponent<MeshRenderer>().material = assetReferenceMaterial.Asset as Material;
 
    m_go = go.Result;
}
 

그러나 결과적으론 비정상적으로 동작하므로 null 체크를 통해 방어 코드를 작성하자.

어드레서블의 Release 플로우

정리를 하면서 Release플로우가 이럴 것이라고 다음과 같이 예상을 해보았다.

결론부터 말하자면 해당 에셋의 Ref Count가 0이 될 때까지 기다렸다가 자동으로 해제한다


에셋에게 ReleaseAsset 함수를 실행시키면 에셋이 Scene Memory의 참조 카운트를 살핀다.

만약 해당 에셋을 Scene Momory에서 참조하고 있으면 Release 함수를 SceneMomory의 개체들에게 위임한다.


그리고 개체들이 ReleaseInstnace를 호출할 때마다 Ref Count를 체크하고 0이되면 해당 에셋을 Release한다.

단, 개체들이 ReleaseInstnace을 호출하려면 어드레서블로 생성된 개체들이어야 한다.

FilteredReferences 스크립트도 SceneMemory에 GameObject로써 존재하지만 Release 했을 때 기다려주지 않고 바로 해제해버린다. 왜냐하면 FilteredReferences는 Instatiate된 개체가 아니기 때문이다. 즉, Ref Count에 영향을 주지 않는 개체인 것이다.

 

실제 메모리에선 에셋이 존재하지만 해당 어드레스 필드는 Null로 채워진다.

FilteredReferences 스크립트의 m_materaial은 로드된 에셋의 '참조값'이다. Release가 호출되면 곧바로 어드레서블의 Asset은 Null이 되지만 실제론 memory에 존재하기 때문에 m_materaial은 아직 null이 아니다.

그래서 위 코드처럼 assetReferenceMaterial.Asset 대신 m_material을 참조하면 정상적으로 동작한다. 

"결과적으로 캐싱해서 쓰는 것이 안전"

 

마지막으로, 에셋을 중복 로드하면 Release할 수 없으니 2회 이상 중복 로드하지 않도록 주의하자.

댓글