목차
스파르타 메타버스 만들기 과제 MetaBUS 트러블 슈팅
씬 전환과 상태 복원 문제 해결 (LoadScene, Additive, PlayerPrefs, sceneLoaded 콜백)
배경
유니티에서 메인 씬과 플래피 미니게임 씬을 전환하는 과정에서 씬이 완전히 전환되어 메인 씬이 초기 상태로 돌아가는 상황이었습니다.
미니게임을 잠깐 '들어갔다 나오는' 느낌이 아니라, 완전히 새로 시작하는 듯한 플레이는 유저 경험에 좋지 않을 것 같았습니다.
발단
SceneManager.LoadScene("MainScene") 호출은 메인 씬을 완전히 다시 로드하기 때문에 기존 플레이어 위치나 UI 상태가 모두 초기화됩니다.
이를 개선하기 위해 LoadSceneMode.Additive를 사용해 미니 게임 씬을 추가로 로드하고, UnloadSceneAsync로 미니게임 씬만 언로드하는 방식을 시도했습니다.
// 미니게임 씬 추가 로드
SceneManager.LoadScene("FlappyGameScene", LoadSceneMode.Additive);
// 미니게임 씬 언로드
SceneManager.UnloadSceneAsync("FlappyGameScene");
전개
하지만 두 씬이 동시에 존재하며 같은 공간에서 겹쳐서 보이는 문제가 발생했고, 이를 해결하려 두 번째 씬 오브젝트들을 x 축으로 100만큼 이동시켰습니다.
하지만 메인 씬과 미니게임 씬 각각의 입력 방식 (WASD vs 마우스 클릭)이 섞여 의도치 않은 입력이 발생했고, 씬에 이벤트 시스템이 두 개 존재하게 되어 혼란이 예상되었습니다.
더불어, 마지막 씬 언로드 시 에러가 발생하는 문제도 예상되었습니다.
위기
이러한 문제들로 인해 Additive 씬 로드 방식은 여러 단점들이 발견되었고, 프로젝트 특성상 적합하지 않아 결국 폐기하였습니다.
대안: PlayerPrefs 이용 상태 저장 및 씬 완전 전환
미니게임 진입 전 플레이어 위치, UI 상태, 점수 등 필요한 정보를 PlayerPrefs에 저장하고, 씬 전환 후 복원하는 방식을 선택하였습니다.
// 저장
PlayerPrefs.SetFloat("PlayerX", player.transform.position.x);
PlayerPrefs.SetFloat("PlayerY", player.transform.position.y);
PlayerPrefs.SetFloat("PlayerZ", player.transform.position.z);
PlayerPrefs.SetInt("UIState", (int)uIManager.currentState);
SceneManager.LoadScene("FlappyGameScene");
// 복원
if (PlayerPrefs.HasKey("PlayerX"))
{
float playerX = PlayerPrefs.GetFloat("PlayerX");
float playerY = PlayerPrefs.GetFloat("PlayerY");
float playerZ = PlayerPrefs.GetFloat("PlayerZ");
int uiState = PlayerPrefs.GetInt("UIState");
Init(); // UIManager, PlayerController 초기화
player.transform.position = new Vector3(playerX, playerY, playerZ);
uIManager.SetState((UIState)uiState);
}
SceneManager.LoadScene("MainScene");
문제점 발견: 씬 전환 후 오브젝트 접근 시점
GameManager는 DontDestroyOnLoad()로 유지되어 초기화가 한 번만 실행되므로, 씬 전환 시 메인 씬의 오브젝트들이 아직 생성되지 않아 player나 uIManager가 null이 되는 문제가 있었습니다.
해결: SceneManager.sceneLoaded 콜백 활용
씬 로드가 완전히 끝난 후 복원을 실행하도록 sceneLoaded 이벤트에 콜백을 등록했습니다.
UnityAction<Scene, LoadSceneMode> sceneLoadCallback = null;
sceneLoadCallback = (scene, mode) =>
{
if (scene.name == "MainScene")
{
Init(); // 오브젝트 재탐색 및 초기화
player.transform.position = new Vector3(playerX, playerY, playerZ);
uIManager.SetState((UIState)uiState);
SceneManager.sceneLoaded -= sceneLoadCallback; // 콜백 해제
}
};
SceneManager.sceneLoaded += sceneLoadCallback;
SceneManager.LoadScene("MainScene");
결말
sceneLoaded 콜백을 활용해 씬 로드 완료 시점에 필요한 초기화를 수행하여 null 참조 문제를 해결하고, 안정적으로 플레이어 위치와 UI 상태를 복원할 수 있었습니다.
Additive 씬 전환 방식의 단점과 에러를 피하면서도 원하는 '들어갔다 나온다'는 느낌을 구현할 수 있었습니다.
Coroutine을 사용한 Dialogue 반응 처리 문제 해결
배경
Unity에서 오브젝트와 상호 작용 시 DialogueUI를 띄우고, 유저의 반응을 받아야하는 기능을 구현 중이었습니다.
기존에는 Update() 내에서 userResponse.HasValue를 지속적으로 체크하여 진행하려 했습니다.
발단
하지만 Update()에서 매 프레임마다 상태를 검사하는 방식은 성능 저하 우려와 코드 가독성 문제를 일으킬 것 같았습니다.
더 효율적이고 깔끔한 방법을 고민하던 중, 코루틴(Coroutine)을 활용하는 방식을 시도하기로 했습니다.
전개
전개
public IEnumerator WaitForDialogueResponse(string message)
{
this.SetActive(GetUIState()); // 현재 다이얼로그 UI 활성화
this.message.text = message;
userResponse = null;
yield return new WaitUntil(() => userResponse.HasValue);
this.gameObject.SetActive(false);
}
위와 같이 DialogueUI 스크립내 내에 WaitForDialogueResponse 코루틴을 작성하였습니다.
그리고 다음과 같이 UIManager에서는 코루틴의 실행을 담당하는 StartDialogueCoroutine 메서드를 추가해, 호출부에서 쉽게 다이얼로그를 실행할 수 있도록 하였습니다.
public void StartDialogueCoroutine(string message)
{
StartCoroutine(dialogueUI.WaitForDialogueResponse(message));
}
위기
네이밍 컨벤션과 역할 분담에 대한 고민이 있었습니다.
DialogueUI의 다이얼로그 표시 메서드 이름은 기존 DisplayDialogue에서 코루틴을 명확히 하기 위해 WaitForDialogueResponse로 변경하였고,
UIManager에서도 코루틴임을 표현하기 위해 StartDialogueCoroutine으로 이름을 변경하였습니다.
결말
코루틴을 활용한 대기 방식 덕분에, Update()에서 반복 검사하는 방식보다 코드가 훨씬 깔끔해지고, 논리 흐름도 명확해졌습니다.
UI 활성화 및 비활성화 처리도 자연스럽게 이어져 성능 및 가독성 측면에서 개선 효과가 있었습니다
Coroutine에서 Action<T> 콜백을 사용해 결과 반환 문제 해결
배경
코루틴을 사용하여 유저의 대화 반응을 기다릴 때, yield return은 값을 직접 반환하지 못하는 한계가 있었습니다.
이 때문에 코루틴 내에서 결과를 외부로 전달하는 방법이 필요했습니다.
발단
코루틴에서는 반환값을 직접 받을 수 없으므로, 호출자에게 유저 반응 결과를 알려주려면 별도의 콜백 메커니즘이 필요했습니다.
간단하게 사용할 수 있는 Action<T>를 활용하기로 결정했습니다.
전개
public IEnumerator WaitForDialogueResponse(string message, Action<bool> callback)
{
this.SetActive(GetUIState()); // 현재 다이얼로그 UI 활성화
this.message.text = message;
userResponse = null;
yield return new WaitUntil(() => userResponse.HasValue);
Debug.Log(userResponse + " 눌림");
this.gameObject.SetActive(false);
callback.Invoke(userResponse.Value); // Nullable 값만 전달
}
DialogueUI클래스에서 WaitForDialogueResponse 코루틴에 Action<bool> 타입의 파라미터를 추가하였고, 유저가 응답을 완료하면 콜백을 호출하도록 구현하였습니다.
그리고 다음과 같이 UIMaanger에서는 이 코루틴을 시작하는 메서드를 아래처럼 수정하여 콜백을 인자로 받도록 했습니다.
public void StartDialogueCoroutine(string message, Action<bool> callback)
{
StartCoroutine(DialogueUI.WaitForDialogueResponse(message, callback));
}
호출부인 PlayerController.se에서는 다음과 같이 람다식을 통해 콜백을 전달하며, 유저의 선택에 따른 로직을 분기 처리하였습니다.
UIManager.Instance.StartDialogueCoroutine(message, (bool response) =>
{
if (response)
{
SceneManager.LoadScene("FlappyGameScene");
}
else
{
string message = "Okay, it looks safer not to drink";
UIManager.Instance.ActivateGameMessageUI(message);
}
});
결말
Action<T> 콜백을 도입함으로써 코루틴 내부에서 결과를 반환하지 못하는 한계를 극복하고, 비동기적으로 유저 반응을 외부로 전달할 수 있게 되었습니다.
코드 가독성도 좋아지고, 무엇보다 호출자 쪽에서 유연하게 후속 처리 로직을 작성할 수 있게 되어 추후 마법사와의 대화에서 활용하는 등 개발 효율이 향상되었습니다.
PlayerInput.ActivateInput()으로 유저 입력 제어 문제 해결
배경
Unity Input System을 사용하는데, Start 버튼이 클릭 되기 전에도 WASD 입력으로 캐릭터가 움직이는 문제가 있었습니다.
발단
Time.timeScale= 0 을 사용했지만 애니메이션까지 멈춰 자연스럽지 않았습니다.
준비 상태에선 Idle 애니메이션은 유지하면서 이동 입력만 막고 싶었고, 시간 멈춤 방식으로는 불가능했습니다.
전개
// PlayerController.cs
public void SetPlayerInput(bool isEnable)
{
if (playerInput == null)
{
Debug.LogError("PlayerInput is not assigned in PlayerController.");
return;
}
if (isEnable)
playerInput.ActivateInput();
else
playerInput.DeactivateInput();
}
PlayerInput의 ActivateInput()과 DeactivateInput() 메서드로 입력 활성화/비활성화를 제어했습니다.
또한 다음과 같이 GameManager에서 래핑해 쉽게 호출할 수 있게 했습니다.
// GameManager.cs
public void SetPlayerInput(bool enabled)
{
player.SetPlayerInput(enabled);
}
StarUI가 활성화 되면 입력을 막고, 비활성화되면 입력을 허용하도록 했습니다.
public void OnEnable() // StartUI 활성화 시 입력 비활성화
{
GameManager.Instance.SetPlayerInput(false);
}
public void OnDisable() // StartUI 비활성화 시 입력 활성화
{
GameManager.Instance.SetPlayerInput(true);
}
결말
게임 시작 전엔 플레이어 입력이 차단되고 Idle 애니메이션은 자연스럽게 유지되어 준비 상태임을 잘 표현할 수 있었습니다.
명확한 입력 제어로 코드도 깔끔해졌으며, 다른 입력 제어 순간이 필요한 경우에도 손쉽게 사용할 수 있게 되었습니다.