Streaming
데이터를 연속적으로 전송하여 실시간으로 재생하는 일
Scene Streaming
Unity 속 Scene Loading 하지 않고, 같은 Scene에서 다른 Scene을 추가하여(Additive) 전체 Scene을 렌더링하는 방식
5 * 5의 Sector로 구성되어있는 맵입니다.
각각의 Sector를 좌표로서 구성하고, Scene 이름은 Sector_x_y로 되어있니다.
그럼 총 25개의 Sector Scene이 만들어 집니다.

이 Sector 들은 Player의 위치를 기반으로 Player 주변 Sector가 Load되고, 멀어지면 UnLoad 됩니다.
이렇게 OpenWorld와 같이 방대한 양의 데이터를 가진 게임은 대부분의 데이터를 디스크에 저장해 두었다가 필요한 부분만 메모리에 담아 메모리를 절약합니다.
MapStreaming Manager.sc
public static MapStreamingManager Instance { get; private set; }
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public Vector2Int GetSector(Vector3 position)
{
int x = Mathf.FloorToInt((position.x + sectorSize / 2) / sectorSize);
int y = Mathf.FloorToInt((position.z + sectorSize / 2) / sectorSize);
return new Vector2Int(x, y);
}
int x = Mathf.FloorToInt((position.x + sectorSize / 2) / sectorSize);
[position.x + sectorSize/2]의 목적
위 좌표가 설정된 섹터처럼 호출하기 위해서 섹터의 중심점을 재배치 한 것 입니다.
//Load 상태 저장 Dictionary
public static Dictionary<Vector2Int, bool> LoadedSectorState = new Dictionary<Vector2Int, bool>();
void LoadSectorFunc(Vector2Int currentSector)
{
for (int x = -loadDistance; x <= loadDistance; x++)
{
for (int y = -loadDistance; y <= loadDistance; y++)
{
Vector2Int sectorToLoad = currentSector + new Vector2Int(x, y);
if (CheckValidSector(sectorToLoad))
{
if (!LoadedSectorState.ContainsKey(sectorToLoad))
{
LoadedSectorState.Add(sectorToLoad, false);
}
if (!LoadedSectorState[sectorToLoad])
{
LoadedSectorState[sectorToLoad] = true;
StartCoroutine(LoadSector(sectorToLoad));
}
}
}
}
}
IEnumerator LoadSector(Vector2Int sectorToLoad)
{
string sceneName = $"Sector_{sectorToLoad.x}_{sectorToLoad.y}";
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (!asyncLoad.isDone)
{
yield return null;
}
asyncLoad.completed += (AsyncOperation op) =>
{
Debug.Log($"Scene '{sceneName}' has been loaded Event");
};
LoadedSectorState[sectorToLoad] = true;
}
👉🏻 상태 정보 갱신
LoadedSectorState[sectorToLoad] = true;
문제
LoadSector를 코루틴을 통해 비동기로 불러오는 방식을 채용했을 경우. Update를 통해 중복 호출이 되는 문제가 발생하게 됩니다.
해결
LoadSectorState Dictionary<>가 존재하는 이유입니다. Sector를 호출하는 비동기 작업을 수행하기 전에 활성화 상태를 갱신하면 비동기 동시 호출을 방지 할 수 있습니다.
void UnloadSectorFunc(Vector2Int currentSector)
{
foreach (var sector in LoadedSectorState.Keys)
{
if (CheckValidSector(sector))
{
if (Mathf.Abs(sector.x - currentSector.x) > loadDistance || Mathf.Abs(sector.y - currentSector.y) > loadDistance)
{
if (LoadedSectorState[sector])
{
LoadedSectorState[sector] = false;
StartCoroutine(UnloadSector(sector));
}
}
}
}
}
IEnumerator UnloadSector(Vector2Int sectorToUnLoad)
{
string sceneName = $"Sector_{sectorToUnLoad.x}_{sectorToUnLoad.y}";
AsyncOperation asyncUnload = SceneManager.UnloadSceneAsync(sceneName);
while (!asyncUnload.isDone)
{
yield return null;
}
asyncUnload.completed += (AsyncOperation op) =>
{
Debug.Log($"Scene '{sceneName}' has been Unloaded Event");
};
UnLoadSceneTask.Remove(sectorToUnLoad);
LoadedSectorState[sectorToUnLoad] = false;
}

위 사진 처럼 LoadDistance의 크기에 따라 Sector를 로드할 수 있는 범위가 설정 됩니다. PC의 사양 및 게임 설계에 맞게 설정해주시면 됩니다.

loadDistance = n
총 Load되는 sector의 수 = (2n+1) * (2n+1)
loadDistance = 2
캐릭터가 위치한 Sector를 기준으로 +/- 2 씩 하여 총 25개의 Sector가 Load 되는 것을 볼 수 있습니다.
public class MapStreamingManager : MonoBehaviour
{
public static MapStreamingManager Instance { get; private set; }
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public Transform player;
public int sectorSize = 200;
public int loadDistance = 1;
private void Update()
{
Vector2Int currentSector = GetSector(player.position);
Debug.Log($"사용자의 현재 Sector : ({currentSector.x}, {currentSector.y})");
LoadSectorFunc(currentSector);
UnloadSectorFunc(currentSector);
}
Vector2Int maxSector = new Vector2Int(2, 2);
Vector2Int minSector = new Vector2Int(-2, -2);
//유효성 검사 메서드
bool CheckValidSector(Vector2Int sector)
{
return (sector.x >= minSector.x && sector.x <= maxSector.x && sector.y >= minSector.y && sector.y <= maxSector.y);
}
public static Dictionary<Vector2Int, bool> LoadedSectorState = new Dictionary<Vector2Int, bool>();
void LoadSectorFunc(Vector2Int currentSector)
{
for (int x = -loadDistance; x <= loadDistance; x++)
{
for (int y = -loadDistance; y <= loadDistance; y++)
{
Vector2Int sectorToLoad = currentSector + new Vector2Int(x, y);
if (CheckValidSector(sectorToLoad))
{
if (!LoadedSectorState.ContainsKey(sectorToLoad))
{
LoadedSectorState.Add(sectorToLoad, false);
}
if (!LoadedSectorState[sectorToLoad])
{
LoadedSectorState[sectorToLoad] = true;
StartCoroutine(LoadSector(sectorToLoad));
}
}
}
}
}
void UnloadSectorFunc(Vector2Int currentSector)
{
foreach (var sector in LoadedSectorState.Keys)
{
if (CheckValidSector(sector))
{
if (Mathf.Abs(sector.x - currentSector.x) > loadDistance || Mathf.Abs(sector.y - currentSector.y) > loadDistance)
{
if (LoadedSectorState[sector])
{
LoadedSectorState[sector] = false;
StartCoroutine(UnloadSector(sector));
}
}
}
}
}
public Vector2Int GetSector(Vector3 position)
{
int x = Mathf.FloorToInt((position.x + sectorSize / 2) / sectorSize);
int y = Mathf.FloorToInt((position.z + sectorSize / 2) / sectorSize);
return new Vector2Int(x, y);
}
IEnumerator LoadSector(Vector2Int sectorToLoad)
{
string sceneName = $"Sector_{sectorToLoad.x}_{sectorToLoad.y}";
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (!asyncLoad.isDone)
{
yield return null;
}
asyncLoad.completed += (AsyncOperation op) =>
{
Debug.Log($"Scene '{sceneName}' has been loaded Event");
};
LoadedSectorState[sectorToLoad] = true;
}
IEnumerator UnloadSector(Vector2Int sectorToUnLoad)
{
string sceneName = $"Sector_{sectorToUnLoad.x}_{sectorToUnLoad.y}";
AsyncOperation asyncUnload = SceneManager.UnloadSceneAsync(sceneName);
while (!asyncUnload.isDone)
{
yield return null;
}
asyncUnload.completed += (AsyncOperation op) =>
{
Debug.Log($"Scene '{sceneName}' has been Unloaded Event");
};
UnLoadSceneTask.Remove(sectorToUnLoad);
LoadedSectorState[sectorToUnLoad] = false;
}
}