멈추지 않고 끈질기게
[Unity][Photon][포트폴리오] 씬 로딩 동기화하기 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 Unity 2021.3.15f1 / Pun 2.41 / Photon lib 4.1.6.17 버전을 기준으로 작성되었습니다.
※ 개발 중인 유니티 3D 포트폴리오의 소스 코드를 포함하고 있습니다.
1. 포톤 네트워크를 이용한 씬 호출
기존에는 멀티플레이 시 RPC를 통해 같은 인덱스의 씬을 로드하도록 했으나, 포톤 SDK에서 지원하는 PhotonNetwork.LoadLevel()을 사용하는 방향으로 로직을 변경하기로 했습니다. 해당 함수를 사용하면 같은 방에 속한 다른 클라이언트들도 해당 씬을 로드하며, 로딩 프로세스 동안 네트워크 메시지를 일시정지하므로 좀 더 안정적인 씬 호출이 될 것으로 판단했습니다. 다만 처음 사용해보니 아무래도 이슈가 많이 발생했고, 이번 포스팅은 그 이슈들과 해결 과정에 대한 내용입니다.
2. 1차 수정(LoadLevel())
우선 기존에 사용하던 스테이지 진입 로직은 다음과 같습니다.
IEnumerator LoadingStage()
{
//플레이어 씬 로드 대기
AsyncOperation op = SceneManager.LoadSceneAsync(Convert.ToInt32(SceneIndex.Player), LoadSceneMode.Additive);
while (!op.isDone)
{
yield return WfsManager.Instance.GetWaitForSeconds(minInterval);
}
//스테이지 씬 로드 대기
op = SceneManager.LoadSceneAsync(curStageIdx, LoadSceneMode.Additive);
while (!op.isDone)
{
yield return WfsManager.Instance.GetWaitForSeconds(minInterval);
}
loadingScreen.SetActive(false);
}
함수는 코루틴으로 선언하고, AsyncOperation 타입을 반환하는 SceneManager.LoadSceneAsync()를 이용하여 씬을 호출했습니다. 반환값을 AsyncOperation 변수에 저장한 뒤, isDone을 체크하며 씬 로딩이 끝날때까지 대기하고, 끝나면 다음 으로 넘어가는 방식입니다.
위 함수를 PhotonNetwork.LoadLevel()을 사용하는 방식으로 간단하게 다음과 같이 변경했습니다. 그리고 PhotonNetwork를 통해 자동으로 씬 동기화가 되도록 NetworkManager 클래스의 초기화 부분에서 PhotonNetwork.AutomaticallySyncScene 값을 true로 설정하고, 씬 로딩 함수는 마스터 클라이언트에서만 실행하도록 변경했습니다.
using PN = Photon.Pun.PhotonNetwork;
IEnumerator LoadingStage()
{
//플레이어 씬 로드
PN.AsyncLoadLevel(Convert.ToInt32(SceneIndex.Player));
// 스테이지 씬 로드
PN.AsyncLoadLevel(curStageIdx);
loadingScreen.SetActive(false);
}
하지만 씬 로딩 방식에서 이슈가 있었습니다. 현재 프로젝트에서는 스테이지 진입 시 오브젝트를 배치한 스테이지 씬과 UI 부분을 담당하는 플레이어 씬을 함께 불러오는 방식을 사용하고 있었는데(SceneMode.Additive), PhotonNetwork.LoadLevel()은 확인해보니 SceneMode.Single 형태로 로드하는 함수였습니다. 그래서 우선 해당 함수를 SceneMode.Additive로 로드하도록 수정했습니다.
public static void LoadLevel(int levelNumber)
{
if (PhotonHandler.AppQuits)
{
return;
}
if (PhotonNetwork.AutomaticallySyncScene)
{
SetLevelInPropsIfSynced(levelNumber);
}
PhotonNetwork.IsMessageQueueRunning = false;
loadingLevelAndPausedNetwork = true;
// LoadSceneMode.Single -> LoadSceneMode.Additive로 변경
_AsyncLevelLoadingOperation = SceneManager.LoadSceneAsync(levelNumber,
LoadSceneMode.Additive);
}
허나 이번엔 씬이 무한 로딩에서 벗어나지 못하는 이슈가 발생했습니다. LoadLevel() 함수의 주석을 읽어보니 해당 함수로 씬 로딩 중에 다른 씬을 호출할 경우, 기존의 로딩을 취소한다는 내용이 있었습니다.
/// Calling LoadLevel before the previous scene finished loading is not recommended.
/// If AutomaticallySyncScene is enabled, PUN cancels the previous load (and prevent that from
/// becoming the active scene). If AutomaticallySyncScene is off, the previous scene loading can finish.
/// In both cases, a new scene is loaded locally.
결국, 플레이어 씬을 로드하고 끝나기 전에 바로 스테이지 씬을 로드하는 바람에 생긴 이슈였습니다. 따라서 기존 로직처럼 플레이어 씬을 로드하고 끝날때까지 기다린 뒤에 스테이지 씬을 로드해야 할 필요성이 있었습니다. 그리고 LoadLevel()도 결국 SceneManager.LoadSceneAsync()를 이용해서 로드한다는 점에 착안해서, 아예 새롭게 함수를 선언하였습니다.
public static AsyncOperation AsyncLoadLevel(int levelNumber)
{
if (PhotonHandler.AppQuits)
{
return null;
}
if (PhotonNetwork.AutomaticallySyncScene)
{
SetLevelInPropsIfSynced(levelNumber);
}
PhotonNetwork.IsMessageQueueRunning = false;
loadingLevelAndPausedNetwork = true;
_AsyncLevelLoadingOperation = SceneManager.LoadSceneAsync(levelNumber, LoadSceneMode.Additive);
return _AsyncLevelLoadingOperation;
}
함수의 내용은 LoadLevel()과 동일하고, 대신 반환형을 AsyncOperation으로 만든 함수입니다. 기존 함수에서도 LoadSceneAsync()의 반환값을 _AsyncLevelLoadingOperation라는 static 변수에 저장하고 있었는데, 해당 변수가 private이라 아예 함수의 반환값으로 리턴하도록 했습니다. 해당 함수를 이용해서 기존의 씬 로딩 로직에 그대로 적용하고, 로딩 스크린의 해제만 각 클라이언트에서 별도로 실행하도록 수정했습니다.
// 멀티 게임용 로딩 코루틴
// AsyncOperation을 반환하도록 새로 작성한 AsyncLoadLevel() 사용
IEnumerator LoadingStage_Network()
{
//플레이어 씬 로드 대기
AsyncOperation op = PN.AsyncLoadLevel(Convert.ToInt32(SceneIndex.Player));
while (!op.isDone)
{
yield return WfsManager.Instance.GetWaitForSeconds(minInterval);
}
//스테이지 씬 로드 대기
op = PN.AsyncLoadLevel(curStageIdx);
while (!op.isDone)
{
yield return WfsManager.Instance.GetWaitForSeconds(minInterval);
}
}
// StageController에서 호출(Start())
// 로딩 스크린 해제
public void LoadingCompleted()
{
loadingScreen.SetActive(false);
}
3. 2차 수정(AutomaticallySyncScene)
1차 수정 이후에 로딩에는 이상이 없었으나, 언로드하는 과정에서 이슈가 발생하였습니다. A 클라이언트에서 도중에 나가기를 선택한 경우(결과 씬으로 이동), B 클라이언트가 UI 연출 후 결과 씬으로 이동할 때 A 클라이언트가 타이틀 씬으로 나가버리기 시작했습니다.
원인은 PhotonNetwork.AutomaticallySyncScene에 있었습니다. 로딩을 동기화하기 위해서 해당 값을 true로 설정하였는데, 스테이지 입장 후에는 특정 플레이어의 중도 퇴장 등 씬 로드/언로드가 클라이언트마다 개별적으로 일어날 수 있기 때문이었습니다. 따라서 AutomaticallySyncScene 값은 멀티 게임을 시작하기 전에 true로 설정하고, 스테이지 입장 후에 false로 변경하는 방식으로 수정하였습니다.
// 랜덤 스테이지 입장
// stageIdx는 SetStageIdx로 미리 설정(RPC로 호출)
public void EnterRandomStage()
{
// LoadLevel()을 위해 true로 설정
PN.AutomaticallySyncScene = true;
loadingScreen.SetActive(true);
SceneManager.UnloadSceneAsync(Convert.ToInt32(SceneIndex.Lobby));
// 싱글 게임
if (!NetworkManager.Instance.InRoom)
StartCoroutine(LoadingStage());
// 멀티 게임(마스터 클라이언트만 호출)
if (PN.IsMasterClient)
StartCoroutine(LoadingStage_Network());
}
// StageController에서 호출(Start())
// 로딩 스크린 해제 및 씬 동기화 해제
public void LoadingCompleted()
{
loadingScreen.SetActive(false);
PN.AutomaticallySyncScene = false;
}
4. 결론
위 내용까지 수정한 후, LoadLevel()을 통한 씬 호출이 정상적으로 진행되었습니다. 외부 SDK를 사용하면서 거기에 포함된 함수를 분석하고, 직접 수정해서 사용해본 점은 좋은 경험이 되었습니다.다만 간헐적으로 한쪽 클라이언트에서만 로딩이 늦어지는 현상이 발생하고 있어, 해당 부분은 확인중입니다.
'포트폴리오' 카테고리의 다른 글
[Unreal][공지] 최종 프로젝트 개발 일지 관련 (0) | 2024.05.03 |
---|---|
[포트폴리오] 언리얼 팀 프로젝트 회고(1차) (0) | 2024.04.23 |
[Unity][포트폴리오] 카메라 회전 (0) | 2023.06.22 |
[Unity][포트폴리오] 신규 스테이지 추가 (0) | 2023.06.17 |
[Unity][Graphics][포트폴리오] 기본 그래픽스 설정 (0) | 2023.06.13 |