오디오 매니저에서는 크게 달라진 점은 없었다. 다만 예외처리문을 Debug.Error로 로그를 찍어내서 팀원분께서 에러로 착각하시길래 Debug.Warnig으로 로그를 출력하였다.
여기서는 바뀐 부분이 꽤 많다.
먼저, 클립이름으로 오디오매니저의 딕셔너리 키값을 넘겨주었는데 이것은 나의 의도된 부분이 아니였다. 내가 생각한 것은 어드레서블 주소를 키값으로 딕셔너리에서 그대로 뽑아 사용하려고 하였기에 이 부분을 고쳐야 했다.
수정 전 코드
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
public class LoadAssetManager : Singleton<LoadAssetManager>
{
//비동기 로딩 시 사용할 핸들
private List<AsyncOperationHandle<AudioClip>> loadAudioClipHandles = new();
private List<AsyncOperationHandle<IList<AudioClip>>> loadSceneAudiohandles = new();
// 레이블을 사용해서 에셋번들의 로케이션을 받아오는 메서드
public void LoadAssetBundle(string labelName)
{
Addressables.LoadResourceLocationsAsync(labelName).Completed +=
(handle =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var locations = handle.Result;
Debug.Log($"로케이션 {locations.Count}개 가져옴");
OnLoadAssetsChangeScene(labelName, locations);
}
else
{
Debug.LogError($"로케이션 로드 실패: {labelName}");
}
});
}
// 받아온 로케이션을 통해 에셋을 로드해오는 메서드
public void OnLoadAssetsChangeScene(string lableName, IList<IResourceLocation> locations)
{
//스테이지에 필요한 사운드레이블을 가져오기
Addressables.LoadAssetsAsync<AudioClip>(locations,null).Completed +=
(handle =>
{
loadSceneAudiohandles.Add(handle);
foreach (AudioClip clip in handle.Result)
{
AudioManager.Instance.AudioDictionary.TryAdd(clip.name, clip);
}
});
}
//비동기 오디오클립 로드 메서드
public void LoadAudioClipAsync(string assetName,Action<string> onLoaded)
{
Addressables.LoadAssetAsync<AudioClip>(assetName).Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var clip = handle.Result;
loadAudioClipHandles.Add(handle);
AudioManager.Instance.AudioDictionary.TryAdd(assetName, handle.Result);
onLoaded?.Invoke(assetName); // 로드 완료 후 콜백 호출
}
else
{
Debug.LogError($"AudioClip 로드 실패: {assetName}");
onLoaded?.Invoke(null); // 실패했을 때 처리
}
};
}
// 메모리에 올라온 오디오클립을 릴리즈
private void ReleaseAudioClips()
{
foreach (var handle in loadAudioClipHandles)
{
Addressables.Release(handle);
}
foreach (var handle in loadSceneAudiohandles)
{
Addressables.Release(handle);
}
loadAudioClipHandles.Clear();
loadSceneAudiohandles.Clear();
AudioManager.Instance.AudioDictionary.Clear();
}
}
여기서 레이블을 통해 에셋번들을 로드해오는 메서드 부분을 보면
// 받아온 로케이션을 통해 에셋을 로드해오는 메서드
public void OnLoadAssetsChangeScene(string lableName, IList<IResourceLocation> locations)
{
//스테이지에 필요한 사운드레이블을 가져오기
Addressables.LoadAssetsAsync<AudioClip>(locations,null).Completed +=
(handle =>
{
loadSceneAudiohandles.Add(handle);
foreach (AudioClip clip in handle.Result)
{
AudioManager.Instance.AudioDictionary.TryAdd(clip.name, clip);
}
});
}
가져온 에셋번들을 foreach로 돌면서 오디오매니저의 딕셔너리에 Add해주고 있는데 키 값이 clip의 name으로 되어있다. 이 부분을 고치기 위해서 각각의 어드레서블의 키값을 딕셔너리의 키값으로 주게 만들고 싶었는데 이를 위해서는 로케이션리스트를 순회할 필요가 있었고, 때문에 기존에 에셋번들을 로드해오고 난 뒤의 핸들을 캐싱해주던 부분이 필요가 없어졌다.
수정 후 코드
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
public class LoadAssetManager : Singleton<LoadAssetManager>
{
//비동기 로딩 시 사용할 핸들
private List<AsyncOperationHandle<AudioClip>> loadAudioClipHandles = new();
// 레이블을 사용해서 에셋번들의 로케이션을 받아오는 메서드
public void LoadAssetBundle(string labelName)
{
Addressables.LoadResourceLocationsAsync(labelName).Completed +=
(handle =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var locations = handle.Result;
Debug.Log($"로케이션 {locations.Count}개 가져옴");
OnLoadAssetsChangeScene(labelName, locations);
}
else
{
Debug.LogError($"로케이션 로드 실패: {labelName}");
}
});
}
// 받아온 로케이션을 통해 에셋을 로드해오는 메서드
public void OnLoadAssetsChangeScene(string labelName, IList<IResourceLocation> locations)
{
foreach (var location in locations)
{
// 각 location의 주소를 키로 사용하기 위해 LoadAssetAsync 사용
var handle = Addressables.LoadAssetAsync<AudioClip>(location);
handle.Completed += (clipHandle) =>
{
if (clipHandle.Status == AsyncOperationStatus.Succeeded)
{
AudioClip clip = clipHandle.Result;
string addressKey = location.PrimaryKey;
// 키는 address, 값은 AudioClip
AudioManager.Instance.AudioDictionary.TryAdd(addressKey, clip);
Debug.Log($"{addressKey} 오디오 클립 추가!");
}
else
{
Debug.LogError($"오디오 로딩 실패: {location.PrimaryKey}");
}
};
loadAudioClipHandles.Add(handle);
}
}
//비동기 오디오클립 로드 메서드
public void LoadAudioClipAsync(string assetName,Action<string> onLoaded)
{
if (assetName == "None")
{
Debug.Log("CallLoad : None");
return;
}
Addressables.LoadAssetAsync<AudioClip>(assetName).Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
var clip = handle.Result;
loadAudioClipHandles.Add(handle);
AudioManager.Instance.AudioDictionary.TryAdd(assetName, handle.Result);
Debug.Log($"{assetName} 오디오 클립 추가!");
onLoaded?.Invoke(assetName); // 로드 완료 후 콜백 호출
}
else
{
Debug.LogWarning($"AudioClip 로드 실패: {assetName}");
onLoaded?.Invoke(null); // 실패했을 때 처리
}
};
}
// 메모리에 올라온 오디오클립을 릴리즈
public void ReleaseAudioClips()
{
foreach (var handle in loadAudioClipHandles)
{
Addressables.Release(handle);
}
loadAudioClipHandles.Clear();
AudioManager.Instance.AudioDictionary.Clear();
}
}
여기서 가장 크게 바뀐 코드를 살펴보면
public void OnLoadAssetsChangeScene(string labelName, IList<IResourceLocation> locations)
{
foreach (var location in locations)
{
// 각 location의 주소를 키로 사용하기 위해 LoadAssetAsync 사용
var handle = Addressables.LoadAssetAsync<AudioClip>(location);
handle.Completed += (clipHandle) =>
{
if (clipHandle.Status == AsyncOperationStatus.Succeeded)
{
AudioClip clip = clipHandle.Result;
string addressKey = location.PrimaryKey;
// 키는 address, 값은 AudioClip
AudioManager.Instance.AudioDictionary.TryAdd(addressKey, clip);
Debug.Log($"{addressKey} 오디오 클립 추가!");
}
else
{
Debug.LogError($"오디오 로딩 실패: {location.PrimaryKey}");
}
};
loadAudioClipHandles.Add(handle);
}
}
레이블이름으로 찾은 로케이션 리스트를 foreach로 순회를 돌며 loadAsset을 해주고 나온 handle들 각각의 어드레서블의 키값을 가져와서 딕셔너리에 추가해준 후 handle은 캐싱해둔다. 이후 씬이 변경될 때 릴리즈해주기 위해서 필요하다.
배틀씬에 필요한 오디오클립을 로드해오기위한 배틀씬로더이다. 배틀씬에서는 출현할 플레이어 유닛들과 적유닛에 맞춰서 오디오클립을 불러와야하기 때문에 따로 클래스를 만들어 필요한 오디오클립들을 찾아올 역할을 할 클래스를 만들었다.
using System;
using System.Collections.Generic;
using UnityEngine;
public class BattleSceneLoader : SceneOnlySingleton<BattleSceneLoader>
{
HashSet<string> bundles = new HashSet<string>();
HashSet<string> assets = new HashSet<string>();
public void LoadAssets()
{
foreach (Unit unit in BattleManager.Instance.AllUnits)
{
assets.Add(unit.UnitSo.AttackType.AttackSound.ToString());
assets.Add(unit.UnitSo.AttackType.HitSound.ToString().ToString());
assets.Add(unit.UnitSo.AttackVoiceSound.ToString().ToString());
assets.Add(unit.UnitSo.HitVoiceSound.ToString().ToString());
// LoadAssetManager.Instance.LoadAudioClipAsync(unit.UnitSo.AttackType.AttackSound.ToString(), null);
// LoadAssetManager.Instance.LoadAudioClipAsync(unit.UnitSo.AttackType.HitSound.ToString(), null);
// LoadAssetManager.Instance.LoadAudioClipAsync(unit.UnitSo.AttackVoiceSound.ToString(), null);
// LoadAssetManager.Instance.LoadAudioClipAsync(unit.UnitSo.HitVoiceSound.ToString(), null);
if (unit.UnitSo is EnemyUnitSO)
{
EnemyUnitSO monsterSO = unit.UnitSo as EnemyUnitSO;
if (monsterSO.monsterType != MonsterType.None)
{
// LoadAssetManager.Instance.LoadAssetBundle(monsterSO.monsterType.ToString()+"Sound");
bundles.Add(monsterSO.monsterType.ToString()+"Sound");
}
}
foreach (SkillData skill in unit.SkillController.skills)
{
if(skill==null || skill.skillSo.SFX == SFXName.None) continue;
// LoadAssetManager.Instance.LoadAudioClipAsync(skill.skillSo.SFX.ToString(), null);
assets.Add(skill.skillSo.SFX.ToString());
}
}
foreach (var bundle in bundles)
{
LoadAssetManager.Instance.LoadAssetBundle(bundle);
}
foreach (var asset in assets)
{
LoadAssetManager.Instance.LoadAudioClipAsync(asset,null);
}
}
}
주석 처리된 코드들은 모두 수정처리가 이루어진 코드이다.
코드 흐름을 보면 모든 배틀씬의 유닛들(해당 배틀씬에 등장할 모든 유닛들)을 순회하며, 유닛들의 공격사운드, 피격사운드 등을 가져오고 유닛들의 스킬컨트롤러가 가지고있는 스킬들의 리스트들에 있는 스킬 사운드도 모두 찾아와서 HashSet에 키값을 저장해놓는다. 이후 모든 키값을 찾아오면 이들을 한번에 로드한다.
HashSet을 쓴 이유는 중복된 에셋을 로드해오는 것을 방지하기 위함이다. 어드레서블은 동일한 에셋을 로드해올수있는데 슬라임이라는 몬스터가 10마리 존재하면 슬라임 오디오에 대한 오디오클립을 10번가져오게 되기때문에 모든 어드레서블키값을 중복저장을 못하게하는 HashSet자료형에 저장해놓았다가 마지막에 한번에 에셋들을 로드해오게 만들었다.