부트캠프/본캠프

[내일배움캠프_2025JUL25] MetaBUS 개인 과제

Young_A 2025. 7. 25. 19:40

목차

    MetaBUS 개인 과제

     

    내 과제 프로젝트 이름은 MetaBUS다.

    왜 Metaverse가 아니냐고...?

    과제 프로젝트 생성하던 순간 멘탈이 이상했나봄!

    하지만 Universe만큼 거창하게 만들지 않겠다고 다짐하는 의미에서 MetaBUS 그대로 가져가기로 했다^^

    아이고 창피해라~


    Coroutine 사용해서 Dialogue 반응 받기

    오브젝트와 상호작용을 시도하면 DialogueUI를 띄우고, 유저의 반응을 받아야 한다.

     

    단순히 Update()에서 userResponse.HasValue를 체크하여 진행하려고 했으나...

    뭔가 성능면에서나 코드 가시성 면에서나 만족스럽지 않을 것 같았다.

     

    그래서 따라해보기만 했지 직접 짜본 적 없는 코루틴을 써보기로 했다.

    //DialogueUI.cs
    public IEnumerator WaitForDialogueResponse(string message)
    {
        this.SetActive(GetUIState());   //현재 다이얼로그 액티브
        this.message.text = message;
        userResponse = null;
    
        yield return new WaitUntil(() => userResponse.HasValue);
    
        this.gameObject.SetActive(false);
    }
    
    //UIManager
    public void StartDialogueCoroutine(string message)  //닫는 건 버튼 통해서 하면 됨.
    {
        StartCoroutine(dialogueUI.DisplayDialogue(message));
    }
    
    //호출부
    UIManager.Instance.StartDialogueCoroutine(message);

     

    DialogueUI에서 userResponse에 값이 들어올 때까지 WatiUntil을 이용해 기다려줬고,

    해당 코루틴을 UIManager에서 시작하는 것으로 작성했다.

     

    네이밍도 고민이 컸는데,

    DialogueUI에서는 원래 DisplayDialogue였지만, WaitUntil 코루틴을 사용한다는 것을 확실히 하기 위해 WaitForDialogueResponse로 변경해주었다.

    UIManager에서도 코루틴을 시작한다는 것을 확실히 하기 위해 StartDialogueCoroutine으로 변경해주었다.

     


    Callback Action<T>

    유니티 개발을 하다보니 코루틴은 yield return 을 사용했을 때 결과를 반환하지 못하기 때문에,

    결과를 반환하기 위해서는 콜백 같은 개념을 사용해야한다.

    자주 쓰게 될 것 같아서 한번 짚고 넘어가야 할 것 같다.

     

    콜백(CallBack)은 피호출자가 호출자를 다시 호출하는 것이다.

    보통 Delegate를 이용하지만 조금 더 간단히 사용할 수 있는 Action도 있다.

     

    Action 타입은 입출력이 없는 메소드를 가리킬 수 있고 반환 값이 없는, 미리 선언되어 있는 무명 델리게이트 변수라고 보면 된다.

    코루틴 같은 경우에는 값을 반환할 수 없으므로, Action을 사용해보기로 했다.

     

    따라서, userResponse의 값을 콜백으로 전달해주는 코드를 Action<T>를 이용하여 다음과 같이 짰다!

    //DialogueUI.cs
    public IEnumerator WaitForDialogueResponse(string message, Action<bool> callback)
    {
        this.SetActive(GetUIState());   //현재 다이얼로그 액티브
        this.message.text = message;
        userResponse = null;
    
        yield return new WaitUntil(() => userResponse.HasValue);
    
        Debug.Log(userResponse + " 눌림");
        this.gameObject.SetActive(false);
        callback.Invoke(userResponse.Value);    //Nullable이므로 값만.
    }
    
    //UIManager.cs
    public void StartDialogueCoroutine(string message, Action<bool> callback)  //닫는 건 버튼 통해서 하면 됨.
    {
        StartCoroutine(DialogueUI.WaitForDialogueResponse(message, callback));
    }
    
    //PlayerController.cs
    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);
        }
    });

     


    LoadScene("", LoadSceneMode.Additive) & UnloadSceneAsync()

    SceneManager.LoadScene("MainScene");

     

    미니게임을 종료하고 다시 메인 씬으로 돌아올 때 위와 같이 진행하면 MainScene 처음으로 돌아가서 Start 버튼까지 새로 띄우게 된다.

    미니게임에 잠깐 "들어갔다" 나온 것이 아니라, "전환됨"이 느껴지는 플레이가 되는 것이다.

     

    방법을 찾아보니 LoadSceneMode.Additive와 UnloadScene을 사용하면 된다고 한다.

     

    Unity Documentation에서는 UnloadScene을 중단하고 UnloadSceneAsync를 사용하라고 제안하고 있다.

    //미니게임 씬 추가로 로드할 때
    SceneManager.LoadScene("FlappyGameScene", LoadSceneMode.Additive);    //미니게임 씬을 추가로 로드
    
    //미니게임 씬 종료할 때
    SceneManager.UnloadSceneAsync("FlappyGameScene");

     

    위와 같이 실행했을 때 두 씬이 겹쳐져서 보이는 문제가 발생했다.

    두 씬이 같은 위치에서 진행되고 있기 때문이다.

     

    해결책으로 두번째 씬의 모든 오브젝트들을 x축으로 100 이동해주었다.

    잘 진행되는 듯 보였다.

     

    메인 씬 캐릭터는 WASD에 반응해서 움직이고, Flappy 미니게임에서는 마우스 클릭으로 반응한다.

    두 씬이 모두 열려있으므로 미니게임을 플레이하면서 WASD 키를 조작한 뒤에 미니게임이 종료되면 메인 씬 캐릭터가 바뀌어있다.


    There are 2 event systems in the scene. Please ensure there is always exactly one event system in the scene

    또한 각 씬의 이벤트 시스템이 둘 다 살아있으므로, 추후 머리가 아파질 것이 더욱 자명했다...

     

    Unloading the last loaded scene Assets/Scenes/FlappyGameScene.unity(build index: 1), is not supported. Please use SceneManager.LoadScene()/EditorSceneManager.OpenScene() to switch to another scene.
    또한 테스트를 할 때, MainScene에서 시작하지 않으면, 종료되었을 때 Unload만 되므로 남아 있는 씬이 없어서 에러가 생긴다.

    메인씬에서만 테스트를 진행해도 되지만, 이거 까딱하다가 남아있는 씬 없어서 예기치 못한 에러가 발생하기 딱 좋겠다는 생각이 들었다. (유니티가 폐기한 이유를 알것 같다)

     

    시도해보니 장점보다 단점이 압도적으로 많아서, 이 방법은 폐기하기로 했다.

     


    PlayerPrefs

    돌고 돌아 캐싱 처리를 할까... 

    어차피 플레이어 스코어 정보도 저장해야하니 SaveData 만들 생각하다가..

    과제 가이드 보고 PlayerPrefs 기능을 쓰기로 했다.

     

    //저장
    PlayerPrefs.SetInt("Score", currentScore);
    
    //불러오기
    int score = PlayerPrefs.GetInt("Score", 0);
    
    //삭제
    PlayerPrefs.DeleteKey("Score");
    PlayerPrefs.DeleteAll();	//전체 삭제 주의
    
    //키 값 존재 확인
    PlayerPrefs.HasKey(key)

     

    이유로는

    1. 간단하다! 별도의 처리 없이 전역에서 접근할 수 있다.

    2. 내가 저장할 정보는, 플레이어 위치, 최고 스코어, 활성화중이던 UI 정보가 전부다! 매우 작은 양이라는 뜻이다.

     

    바로 채택 ㄱㄱ

     

    //저장
    public void EnterFlappyGame()
    {
        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");    //미니게임 씬을 추가로 로드
    }
    //로드
    public void ReturnFromFlappyGame()
    {
        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");  //메인 씬으로 돌아가기
    }

     

    MainScene 로드하면서 GameManager는 DontDestoryOnLoad()로 설정되었으므로 Start()에서 진행한 초기화가 되지 않는다.

    그래서 Init()으로 초기화 전용 함수를 만들고, Start()와 ReturnFromFlappyGame()에서 호출해주는 것으로 모든 쪽에서 초기화를 진행해주려고 했다.

    위처럼 ReturnFromFlappyGame 함수를 GameManager.cs에 구현하고, FlappyGameManager에서 호출해주었다.

    그래도, player가 null이라고 자꾸 그런다...

     

    -> 당연함! player랑 uiManager는 씬 전환하면서 파괴했고, 아직 메인 씬 로드가 안되었음! 없는걸 어떻게 쓰라는 거요!

     

    이런 상태였던 것!!

    아래에서 계속!


    UnityAction sceneLoaded callback

    그래서 그걸 사용할 때가 또 왔다! 이렇게 자주 오려고 그 이름도 콜백! 인가보다! 껄껄

     

    기존에 파괴된 메인 씬 내부에 있는 오브젝트들은 LoadScene() 직후까지도 아직 생성되지 않았을 수도 있다.

    시점 상 너무 빨리 player를 찾으려고 한 게 원인 인 것이다.

    UnityAction<Scene, LoadSceneMode> sceneLoadCallback = null;
    sceneLoadCallback = (scene, mode) =>
    {
        if (scene.name == "MainScene")
        {
            Init(); // 다시 오브젝트를 찾아서 초기화
            if (player == null)
                player = FindObjectOfType<PlayerController>();
            else
                player.transform.position = new Vector3(playerX, playerY, playerZ);
    
            if (uIManager == null)
                uIManager = FindObjectOfType<UIManager>();
            else
                uIManager.SetState((UIState)uiState);
    
            SceneManager.sceneLoaded -= sceneLoadCallback; // 콜백 해제
        }
    };
    SceneManager.sceneLoaded += sceneLoadCallback;
    SceneManager.LoadScene("MainScene");


    따라서 SceneManager.sceneLoaded에 콜백을 등록해서 씬 로드 완료 시점에 복원 처리를 하도록 했다.

    이 과정에서 그냥 Action을 쓸수는 없었고 UnityAction을 사용해야했다.

    그 차이점을 찾아봤는데 아직은 애매하게 알 듯 모를 듯 해서 일단은 GTD 리스트에 올려뒀다.

     


    PlayerInput.ActivateInput() 로 유저 입력 멈추기

     

    메인씬을 시작하기 전에 Start 버튼을 만들었었다. (왜지?)

    Start 버튼을 누르기도 전에 WASD를 눌러서 캐릭터를 움직일 수 있다.

    Time을 멈추면 되지만, 그렇게 되면 캐릭터 애니메이션이 멈춰져있다.

    Idle 애니메이션이 재생이 되고 있어야 게임이 준비되어있다는 느낌을 줄 수 있을 것 같아서 아예 입력을 막는 방법을 찾아보았다.

     

    내 프로젝트의 경우는 Input System을 이용하고 있었기 때문에 매우 간단했다.

    //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();
    }
    
    //GameManager.cs
    public void SetPlayerInput(bool enabled)
    {
        player.SetPlayerInput(enabled);
    }

     

    위와 같이 구현하고, 원하는 구간에서 호출하면 된다!

    나 같은 경우는 StartUI가 활성화될 때는 입력을 막고, 비활성화될 때 는 입력을 받을 수 있도록 하고 싶었기 때문에 다음과 같이 사용했다.

    public void OnEnable()  //StartUI가 활성화될 때 플레이어 입력을 비활성화
    {
        GameManager.Instance.SetPlayerInput(false);
    }
    public void OnDisable() //StartUI가 비활성화될 때 플레이어 입력을 활성화
    {
        GameManager.Instance.SetPlayerInput(true);
    }

     


    느낀점

    일단, 유니티에 익숙한 것도 그렇지만 그 외의 C# 개념에 대해서도 아직 모르는 게 너무 많다.

    웹 개발도 제대로 된 경력이 아니라 코업 경험만 있는데다가 그마저도 오래되었기 때문인가..

    고급 개념들이 많이 약한 느낌이다.

    그럴만도 하다... 막학기 쯤에는 코로나 때문에 제대로 공부 안하고 졸업했으니까...

    그 업보빔을 지금 맞는 게 다행인 거라고 생각해야지 뭐.. 회사 가서 맞으면 더 아프다.


    내일 학습 할 것은 무엇인지

    개인과제가 급한 것도 맞지만, 필수 과제를 끝내면 도전 과제를 진행하기 전에,

    델리게이트 콜백, 코루틴과 같은 자주 쓰이는 개념들을 한번 더 학습 및 복습 해야할 것 같다.