2학년 2학기 교내 엔진응용 수업시간에 배운 내용을 다루었습니다.
빈 게임 씬에 로우폴리 맵과 캐릭터 에셋을 넣고 빌드 후 실행해보았다.
맵 에셋과 이것저것을 넣고 실행하니 약 35메가정도 메모리가 늘어났다.
로우폴리 에셋이라 이정도지 만약 용량이 큰 에셋이었으면 메모리를 훨씬 더 많이 차지하여 시작할 때 매우 느려질 수 있었다.
그렇다면 Hierachy에 직접 할당하지 않고 동적 할당을 했을 때의 메모리 상태는 어떨까?
using UnityEngine;
using UnityEngine.InputSystem;
public class TestLoader : MonoBehaviour
{
[SerializeField] private GameObject _levelArt;
[SerializeField] private GameObject _player;
[SerializeField] private GameObject _zombie;
private void Update()
{
if (Keyboard.current.qKey.wasPressedThisFrame)
{
Instantiate(_levelArt, Vector3.zero, Quaternion.identity);
Instantiate(_player, Vector3.zero, Quaternion.identity);
Instantiate(_player, Vector3.zero, Quaternion.identity);
}
}
}
인스펙에 모델들을 집어넣고 빌드 후 실행해보면,
Q키를 눌러 생성하지도 않았는데, 빈 씬인 상태보다 메모리를 더 차지하고있다.
Q키를 눌러 생성하면 하이라키에 집어넣어 빌드했을 때와 똑같이 메모리를 차지한다.
즉, 우리가 인스펙터에 드래그 앤 드롭 하여 집어넣은 것 만으로도 메모리 차지가 되고 있다는 것이다.
이번엔 구버전 유니티에서 쓰던 방식인 Resource.Load 방식을 써보았다.
유니티에서 Resource 폴더를 만들고, 그 폴더 안에 위의 에셋들을 모두 넣어준다.
코드를 아래와 같이 고쳐준뒤, 다시 빌드하여 실행시켜본다.
using UnityEngine;
using UnityEngine.InputSystem;
public class TestLoader : MonoBehaviour
{
private void Update()
{
if (Keyboard.current.qKey.wasPressedThisFrame)
{
GameObject a = Resources.Load<GameObject>("Level Art");
GameObject b = Resources.Load<GameObject>("Woman");
GameObject c = Resources.Load<GameObject>("Zombie");
Instantiate(a, Vector3.zero, Quaternion.identity);
Instantiate(b, Vector3.zero, Quaternion.identity);
Instantiate(c, Vector3.zero, Quaternion.identity);
}
}
}
메모리를 확인해보면 처음에 아무것도 없던 씬 상태의 메모리와 똑같다. 메모리 차지가 일어나지 않는것이다.
Addressable을 써야하는 이유
하지만 Resource.Load 방식은 큰 문제가 있다.
- 대용량 리소스 로드가 힘들다.
- 한번 로드된 에셋은 언로드할 방법이 없다.
- 한번 사용되고 두번다시 사용되지 않는 에셋이 게임을 종료할때까지 메모리에 남아있게 된다.
- 로딩된 에셋을 중복 로딩해도 체크할 방법이 없다.
- Mono를 지원하지 않고 IL2CPP를 지원하는 구글 플레이에서는 지원을 하지 않는다. 즉 출시가 불가능하다.
이런 이유들 때문에 유니티에서 따로 지원하는 Addressable(어드레서블) 을 사용해야 한다.
addressable 사용법
위에서 했던 작업을 addressable 방식으로 다시 구현해볼것이다.
패키지 매니저에서 Addressable을 다운로드 받는다.
Level Art 프리팹에 가보면 Addressable 체크박스가 생겼을텐데, 이것을 체크해준다.
최초 클릭시 AddressableAssetsData 폴더가 자동 생성된다.
이제 Level Art를 로드하고 지우는 기능을 만들어보겠다.
여러가지 방법이 있지만 가장 간단하게 관리할 수 있는 AssetReference를 사용
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.InputSystem;
public class TestLoader : MonoBehaviour
{
[SerializeField] private AssetReference _levelRef;
private void Update()
{
if (Keyboard.current.qKey.wasPressedThisFrame)
{
LoadLevel();
}
}
private async void LoadLevel()
{
GameObject levelObj = await _levelRef.LoadAssetAsync<GameObject>().Task;
Instantiate(levelObj, Vector3.zero, Quaternion.identity);
}
}
인스펙터에 체크박스를 체크하여 만들었던 AddressableAsset을 넣어준다.
이렇게 만들어진 레퍼런스는 로드하기 전까지는 메모리 차지를 하지 않는다.
하지만 이미 큐 키를 눌러 에셋 레퍼런스를 생성한 상태에서, 한번 더 큐 키를 눌러 생성하면 오류가 생긴다.
에셋 레퍼런스가 이미 해당 에셋을 로딩완료했는지 검사해야 한다.
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.InputSystem;
public class TestLoader : MonoBehaviour
{
[SerializeField] private AssetReference _levelRef;
private void Update()
{
if (Keyboard.current.qKey.wasPressedThisFrame)
{
LoadLevel();
}
}
private async void LoadLevel()
{
if (!_levelRef.IsValid())
{
GameObject levelObj = await _levelRef.LoadAssetAsync<GameObject>().Task;
Instantiate(levelObj, Vector3.zero, Quaternion.identity);
}
else
{
Instantiate(_levelRef.Asset, Vector3.zero, Quaternion.identity);
}
}
}
위 코드는 IsValid를 사용하여 로드되어있는 에셋 레퍼런스인지 판단하고, 이미 로드되어있는 레퍼런스이면 에셋으로 로드한다.
에셋 파괴
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.InputSystem;
public class TestLoader : MonoBehaviour
{
[SerializeField] private AssetReference _levelRef;
private List<GameObject> list = new ();
private void Update()
{
if (Keyboard.current.qKey.wasPressedThisFrame)
{
LoadLevel();
}
if (Keyboard.current.wKey.wasPressedThisFrame)
{
DestroyAssets();
}
}
private void DestroyAssets()
{
foreach (var level in list)
{
Destroy(level);
}
list.Clear();
}
private async void LoadLevel()
{
if (!_levelRef.IsValid())
{
GameObject levelObj = await _levelRef.LoadAssetAsync<GameObject>().Task;
var obj = Instantiate(levelObj, Vector3.zero, Quaternion.identity);
list.Add(obj);
}
else
{
var obj =Instantiate(_levelRef.Asset, Vector3.zero, Quaternion.identity) as GameObject;
list.Add(obj);
}
}
}
위 코드는 W키를 눌렀을 생성된 모든 에셋이 파괴되는 기능을 추가한 코드이다.
프로파일링
어드레서블은 에셋의 실시간 디버깅을 확인할 수 있는 프로파일러를 제공한다.
프로파일러에서는 실시간으로 프레임, 모노힙, 로드된 에셋의 수와 생성된 시점 등을 확인할 수 있다.
정말 모두 깔끔하게 파괴된것인지 프로파일링하기 위해
Addressable Setting으로 가서 Send Profiler Events를 활성화시켜준다.
Window -> AssetManagement -> Addressabe -> Event viewer를 열어준다.
확인을 해보면,
이 상태에서 W를 눌러 모두 파괴해도 에셋의 로드수는 변함이 없다. 즉 프리팹은 여전히 메모리에 올라가있는것이다.
private void DestroyAssets()
{
foreach (var level in list)
{
//_levelRef.ReleaseInstance(level);
Destroy(level);
}
_levelRef.ReleaseAsset();
list.Clear();
}
위와 같이 에셋을 제거하고 RelaseAsset()함수를 실행하는 코드로 수정하고 다시 프로파일링 해주면 메모리에서
완전히 내려가게 된다. (프로파일러 상에서 프리팹 프로그레스바가 끊겨있다)
'Unity' 카테고리의 다른 글
[C#][Unity] 유용한 기능들 정리 (0) | 2024.08.20 |
---|