Unity 프로젝트에서 메인 화면에서 게임 모드를 선택해 씬을 전환할 때, 이전 게임 상태가 유지되어 예상치 못한 동작이 발생하는 문제가 있었습니다.
이번 포스팅에서는 그 원인과 해결 방법을 공유합니다.
🚨 문제 상황
- 메인 화면에서 싱글 플레이를 선택 후 게임을 진행한다.
- 게임을 끝내거나 중간에 종료해 다시 메인 화면으로 이동한다.
- 2인 플레이(Co-Op)를 선택하면 AI가 수를 두는 문제가 발생한다.
👀 즉, 게임 모드를 변경했음에도 이전 상태가 유지되는 문제!
✍ 기존 코드
// ...
public enum GameType { SinglePlayer, CoOpPlayer }
private GameType currentGameTypeState;
public void ChangeToGameScene(GameType gameType)
{
currentGameTypeState = gameType;
SceneManager.LoadScene("Game");
}
// ...
private void SetTurn(TurnType turnType)
{
switch (turnType)
{
case TurnType.PlayerA:
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnA);
SetupPlayerMove(PlayerType.PlayerA, TurnType.PlayerB);
break;
case TurnType.PlayerB:
_gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnB);
// 문제가 발생한 지점
if (currentGameTypeState == GameType.CoOpPlayer) // 2인플레이(코옵)
{
SetupPlayerMove(PlayerType.PlayerB, TurnType.PlayerA);
}
else // 싱글플레이 인 경우
{
_blockController.OnBlockClickedDelegate = null; // 블럭 클릭 델리게이트 제거
// AI 턴: AI로부터 최적의 수를 받아 바로 처리
var result = MiniMaxAIController.GetBestMove(_board);
if (result.HasValue)
ProcessMove(PlayerType.PlayerB, result.Value.row, result.Value.col, TurnType.PlayerA);
else
{
// ...
}
}
break;
}
}
// ...
🧐 디버깅 과정
🔍 currentGameTypeState
값이 덮어씌워지는 문제
디버그 모드를 통해 처음 currentGameTypeState
을 세팅하는 곳과 SetTurn()
메서드 내부에서 currentGameTypeState
를 확인해보니 중간 어딘가에서 currentGameTypeState
의 값이 변경된다는 사실을 발견했다.
이전에 설정된 상태 값이 어딘가에서 덮어씌여지는 것이라고 생각했다.
currentGameTypeState
이 사용되거나 할당된 곳을 열심히 뒤적여봤지만 다른 곳에서 세팅하고 있진 않았다.
어디서 값이 덮어씌어지는지 찾기 위해 여기저기 Debug.Log()
를 찍어보기 시작했는데 가장 먼저 ChangeToGameScene()
메서드에서부터 체크하기로 했다.
public void ChangeToGameScene(GameType gameType)
{
currentGameTypeState = gameType;
SceneManager.LoadScene("Game");
Debug.Log(currentGameTypeState);
}
// ## 예상 실행 결과
// SinglePlayer
// ## 실제 실행 결과
// CoOpPlayer
// SinglePlayer
👀 이상한 점
Debug.Log()
가 두 번 실행됨 → 씬을 한 번만 로드했는데 왜 두 번 출력될까?- 두 번째 값이
SinglePlayer
→ 새로 전달한 값은CoOpPlayer
인데 다시 초기화 됨
Awake()
단계에서 중복이 발견되면 인스턴스를 파괴하므로 반드시 실행될 Awake()
메서드에서 문제가 발생한 것이라고 추측할 수 있었다.
그럼 GameManager의 부모 클래스인 Singleton으로 가보자.
🔍 Singleton<T>
내부에서 중복 실행 문제
using UnityEngine;
using UnityEngine.SceneManagement;
public abstract class Singleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
// ⚠️ 중복 구독 문제
// 씬 전환시 호출되는 액션 메서드 할당
SceneManager.sceneLoaded += OnSceneLoaded;
}
protected abstract void OnSceneLoaded(Scene scene, LoadSceneMode mode);
}
👀 여기서 발생한 문제
- 씬이 로드될 때마다
Awake()
가 실행됨 - 기존 인스턴스가 살아 있는 상태에서 새로운 인스턴스가 생성됨
Destroy(gameObject)
로 새로운 인스턴스를 제거하지만,SceneManager.sceneLoaded += OnSceneLoaded;
구독은 이미 실행됨- 결국 기존 인스턴스가 다시
OnSceneLoaded()
를 실행하면서 상태가 덮어씌워짐
중복 생성된 인스턴스는 Destroy
되겠지만 문제는 Destroy 된다고해도 SceneManager.sceneLoaded += OnSceneLoaded;
는 실행될 수 있다.
그렇다면 이미 존재하는 인스턴스에서 이미 구독중임에도 중복으로 구독하는 문제가 발생한다.
코드 블럭에서는 생략했지만 GameManager
의 OnSceneLoaded
메서드에서는 게임의 초기화 및 실행을 처리한다.
즉, 기존 인스턴스에서 변경된 상태를 참조해 게임을 초기화 시도하지만 도중에 난입한 새 인스턴스가 다시 게임을 초기화함으로서 문제가 발생했던 것이다.
여기서 기존의 GameManager에서는 새 인스턴스는 상태 값을 전달 받지 못하여 default 값인 SinglePlayer로 세팅된 것.
새로 생성된 인스턴스의 상태들이 유지 되는 이유는 클로저와 관련이 있는데, 여기서 자세히 이야기하기에는 적절하지 않아보인다.
여기까지 알아내면 해결은 간단하다. 중복 구독이 되지 않도록 보장해주면 된다.
✅ 해결 방법: 중복 구독 방지
private void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
// ✅ 인스턴스가 처음 생성될 때만 구독
// 씬 전환시 호출되는 액션 메서드 할당
SceneManager.sceneLoaded += OnSceneLoaded;
}
else
{
Destroy(gameObject);
}
// 삭제...
// SceneManager.sceneLoaded += OnSceneLoaded;
}
✅ 이제는?
✔ 새로운 인스턴스가 생성되더라도 중복 구독이 발생하지 않음
✔ OnSceneLoaded()가 두 번 실행되지 않으므로 잘못된 게임 상태 덮어씌우기 문제 해결
이렇게 하면 인스턴스가 처음 생성될 때만 구독을 추가하므로 새로 생성된 인스턴스에서 중복으로 구독되지 않는다.Destroy(gameObject)
로 파괴될 것이라고 생각하고 코드를 작성한 것이 버그의 원인이라고 할 수 있다.
🔥 결론
✔ 싱글톤의 Awake()에서 중복 구독 등 초기화에 주의 필요!
"씬 전환 시 싱글톤 객체가 유지되면서 이벤트를 중복 구독할 위험이 있다.
C#의 가비지 컬렉터는 참조가 남아있는 객체는 제거하지 않기 때문에 싱글톤 패턴을 사용할 때는 직접적으로 이벤트 구독을 관리해야 한다.
그렇지 않으면, 기존 객체가 남아있는 상태에서 새로운 객체가 추가 구독 되면서 예상치 못한 게임 상태 변경이 발생할 수 있다."
'Develop > TIL' 카테고리의 다른 글
MiniMax 알고리즘 (feat.tic-tac-toe) (0) | 2025.02.14 |
---|---|
[TIL]Redux-persist 새로고침이 발생해도 store state 유지하기 (0) | 2023.12.03 |
[TIL] CSS in JS의 성능에 대한 이야기 (0) | 2023.09.09 |
[TIL] JavaScript의 쓰로틀링과 디바운스 그리고 React (0) | 2023.09.04 |
[TIL] Hamming Codes: 해밍 코드 (0) | 2023.08.27 |