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

어드레서블 에셋 시스템 - 개념: 씬 로드 & 언로드, Component AssetReference

by PlaneK 2020. 6. 24.

씬 로드 & 언로드

씬의 로드와 언로드를 구현해보자.

어드레서블 Key로 로드하기

Addressable Name이 'BouncyBall'인 씬 에셋이 있다.

'BouncyBall'은 어드레서블 네임임과 동시에 'key'에 해당한다. (Label도 key가 될 수 있다)

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
 
public class AddressSceneLoader: MonoBehaviour
{
    public string addressSceneKey = "BouncyBall";
    private SceneInstance _sceneInstance;
 
    public void LoadSceneAsync()
    {
        Addressables.LoadSceneAsync(addressSceneKey, LoadSceneMode.Additive).Completed +=
            (handle) =>
            {
                if (handle.Status == AsyncOperationStatus.Succeeded)
                {
                    _sceneInstance = handle.Result;
                }
                
            };
    }
 
    public void UnloadSceneAsync()
    {
        Addressables.UnloadSceneAsync(_sceneInstance).Completed +=
            (handle) =>
            {
                Resources.UnloadUnusedAssets();
            };
    }
}
 

어드레서블을 string타입인 key로 참조하기 때문에 번거로울 수 있다.

에셋 레퍼런스로 로드하기

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
 
public class AddressSceneLoader: MonoBehaviour
{
    public AssetReference addressScene;
    private AsyncOperationHandle<SceneInstance> _handle;
 
    public void LoadSceneAsync()
    {
        addressScene.LoadSceneAsync(LoadSceneMode.Additive).Completed += 
            (handle) =>
            {
                if (handle.Status == AsyncOperationStatus.Succeeded)
                {
                    _handle = handle;
                }
            };
    }
 
    public void UnloadSceneAysnc()
    {
        Addressables.UnloadSceneAsync(_handle).Completed +=
            (handle) =>
            {
                Resources.UnloadUnusedAssets();
            };
    }
}
 
 

AssetReferece 타입으로 직접 참조하기 때문에 오타에 대한 걱정은 안해도 된다.

Scene을 언로드하는 함수는 여러가지로 오버로드되어있는데 위 예시에는 AsyncOperationHandle과 SceneInstance를 사용했다.

Resources.UnloadUnusedAsset

씬을 언로드한 후의 메모리 상태이다. Cubemap은 해당 씬이 참조한 것들이다.

씬 메모리의 리소스들은 자동으로 해제되는데, Cubemap 에셋은 Unload를 해도 Ref Count가 0인데도 불구하고 그대로 존재한다.

("버그라고 생각되지만...")

Resources.UnloadUnsedAsset 함수를 호출해서 메모리상 할당을 해제해주자.

SpaceShooter 디버깅 - NullReferenceException

씬을 로드할 때마다 해당 널참조 에러가 발생한다.

Behaviour.get_isActiveAndEnabled(UnityEngine.Behaviour)로부터 발생한 걸 보아 씬이 새로 로드 됐는데도 해당 개체가 참조되고 있어서 그런듯하다.

마우스 왼쪽 클릭을 하면 발사체가 생성된다.

프리팹을 생성 중에 씬이 사라지니 널참조 에러가 발생한듯하다. 중복된 키를 주석처리하자.

 

Component AssetReference

Material, ScriptableObject, Sprite 등의 Object는 단독으로 Addressable이 될 수 있다. 

반면 스크립트는 Addressable이 될 수 없으므로 AssetReference로 필터링 할 수 없다.

GameObject를 통해서 특정 Component를 필터링하는 ComponentReference를 구현해보자.

[Serializable]
public class ComponentReferenceColorChanger : ComponentReference<ColorChanger>
{
    public ComponentReferenceColorChanger(string guid) : base(guid) { }
}
 

Filtered AssetReference과 동일하게 작성해주면 된다.

이제 해당 필드는 'ColorChanger'가 붙은 GameObject 외에는 할당할 수 없다.

GameObject가 생성되면서 Result로 'ColorChanger' 컴포넌트를 반환한다.

"생성된 GameObject는 어떻게 참조하지?"

 

ComponentReference<TComponent>

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
#if UNITY_EDITOR
using UnityEditor;
#endif
 
public class ComponentReference<TComponent> : AssetReference
{
    public ComponentReference(string guid) : base(guid)
    {
    }
    
    /* 
     Addressables.ResourceManager.CreateChainOperation를 통해서
     GameObject와 TComponent를 연결하는 작업을 수행한다.
    */
    public new AsyncOperationHandle<TComponent> InstantiateAsync(Vector3 position, Quaternion rotation, Transform parent = null)
    {
        return Addressables.ResourceManager.CreateChainOperation<TComponent, GameObject>(base.InstantiateAsync(position, Quaternion.identity, parent), GameObjectReady);
    }
   
    public new AsyncOperationHandle<TComponent> InstantiateAsync(Transform parent = nullbool instantiateInWorldSpace = false)
    {
        return Addressables.ResourceManager.CreateChainOperation<TComponent, GameObject>(base.InstantiateAsync(parent, instantiateInWorldSpace), GameObjectReady);
    }
    public AsyncOperationHandle<TComponent> LoadAssetAsync()
    {
        return Addressables.ResourceManager.CreateChainOperation<TComponent, GameObject>(base.LoadAssetAsync<GameObject>(), GameObjectReady);
    }
 
    AsyncOperationHandle<TComponent> GameObjectReady(AsyncOperationHandle<GameObject> arg)
    {
        var comp = arg.Result.GetComponent<TComponent>();
        return Addressables.ResourceManager.CreateCompletedOperation<TComponent>(comp, string.Empty);
    }
 
    public override bool ValidateAsset(Object obj)
    {
        var go = obj as GameObject;
        return go != null && go.GetComponent<TComponent>() != null;
    }

    // 에셋을 해당 필드위로 Drag해놓으면 호출되는 함수다. 에셋 타입 체크 로직.
    public override bool ValidateAsset(string path)
    {
#if UNITY_EDITOR
        var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
        return go != null && go.GetComponent<TComponent>() != null;
#else
            return false;
#endif
    }
 
    public void ReleaseInstance(AsyncOperationHandle<TComponent> op)
    {
        // Release the instance
        var component = op.Result as Component;
        if (component != null)
        {
            Addressables.ReleaseInstance(component.gameObject);
        }
 
        // Release the handle
        Addressables.Release(op);
    }
}
 

이 소스는 어드레서블이 기본 제공하지 않는 소스다.

GameObject에 대한 AssetReference가 GetComponent<TComponent>로 'TComponent'라는 스크립트 컴포넌트를 체크하는 식으로 구현해놓은 것 같다.

 

사용하기엔 아직 불안정하다.

해당 필드의 프리팹에서 'ColorChanger'를 제거했는데도 할당(초기화) 값이 유지됐다.

원레라면 Missing이 뜨면서 비워져야 정상이다. 보완 코드가 필요해보인다.

"Filtered AssetReference도 이것도 너무 번거로운데 그냥 Label 사용하는게 더 낫지 않을까?...."

"단순하게 Drag&Drop 시 타입을 필터링 한다는 기능 빼고는 크게 효용을 못느끼겠다."

 

댓글