XR플밍 - 9. UnityEngine2D 인터렉티브 프로그래밍 - 개인 프로젝트 4일차(6/2)

이형원·2025년 6월 2일
0

XR플밍

목록 보기
92/215

0.들어가기에 앞서
이제는 게임의 엔딩까지 들어가야 할 시점이다. 게임의 승리와 패배 조건을 설정하고 게임을 풍부하게 하는 기능을 추가해 나가자.

1. 4일차 작업 정리

드디어 게임 클리어와 게임 오버 UI, 게임 일시 정지 UI와 게임 클리어 존, 포탈 에셋도 구했다.
또 맵 디자인에 본격적으로 들어갔는데, 문득 떠오른 아이디어로 부술 수 있는 오브젝트도 추가했다.

<버그 픽스>

  • 몬스터 추격에서 추가 버그 발견으로 수정함 - 플레이어가 벽 너머로 넘어갔는데도 추격을 멈추지 않는 오류

<리펙토링>

  • 오늘 코드 전체적으로 리펙토링 및 주석 달기 작업을 했다.

<맵 구현>

  • 3-1 맵 텍스트 및 몬스터 배치 수정
  • 3-2 맵 텍스트 및 몬스터 배치 수정

<기능 구현>

  • 타이틀 씬 Bgm 추가
  • 게임 일시정지 및 게임 오버, 게임 클리어 조건
  • 게임 일시정지 UI
  • 게임 오버 및 클리어 UI
  • 씬 전환 포탈 구현
  • 인게임 점수 UI
  • 부술 수 있는 박스를 구현 - 박스를 부수면 포션 아이템이 확정적으로 드랍됨

<추가된 몬스터>

  • 박쥐 몬스터 추가

2. 문제의 발생과 해결과정

오늘은 전체적으로 문제의 발생이 적었고 기능 구현 및 테스트가 중점이 되었다.

2.1 게임의 일시 정지 및 종료 시점 구현

게임의 일시정지 기능 자체는 간단하게 구현할 수 있다. 게임매니저에 일시정지 기능을 구현하고, 핵심이 되는 Time.timeScale = 0으로 설정해 주면 게임 내 모든 오브젝트가 정지한다.

#region GamePause

/// <summary>
/// 게임 일시중지 선언 - bool 변수로 중지여부 변경 가능
/// </summary>
/// <param name="isPause"></param>
public void Pause(bool isPause)
{
    m_GamePaused = isPause;
    if (m_GamePaused) Time.timeScale = 0;
    else Time.timeScale = 1;
}

// 게임 일시중지 여부 확인
public bool IsPause()
{
   return m_GamePaused;
}

#endregion

이와 같이 선언하고 Pause버튼을 눌렀을 때 게임이 일시정지가 가능하도록 구현했다.

이렇게 게임의 일시정지와 종료 시점을 전부 구현하고 나니, 여러모로 코드가 많이 꼬여 있어 상호작용 처리가 복잡해진다는 생각이 들어 전체적으로 코드의 리펙토링과 주석 달기를 해 봐야겠다는 생각에 도달했다.

2.2 리펙토링과 주석 달기

의외로 이게 오늘 작업 중에 상당 시간을 차지한 작업이다.
하지만 이렇게 시간을 들여 한 번 해 두는 게 생각보다 많은 도움이 되었다.

특히나 이번 게임에서는 상태 패턴을 구현해둔 터라 이쪽의 코드가 상당히 난잡하게 되어 있었는데, 여기에서 특히 중복되는 코드를 함수로 합치거나 하는 등의 작업을 진행했다.

기존에는 아래와 같은 코드가 두 번이나 쓰였었다.

// 중복 코드를 부모 오브젝트로 옮겨서 상속( Idle, Walk에서 사용 )
// 몬스터의 탐지 레이더와 애니메이션을 Flip하는 기능
protected void Flip()
{
    if (m_normalMonster.SpriteRenderer.flipX)
    {
        m_normalMonster.SpriteRenderer.flipX = true;
        m_normalMonster.PatrolVec = Vector2.left;
    }
    else
    {
        m_normalMonster.SpriteRenderer.flipX = false;
        m_normalMonster.PatrolVec = Vector2.right;
    }
}

하지만 이걸 함수로 묶은 다음 부모 오브젝트 쪽으로 보내버리니 자식 오브젝트에서의 상태가 더 보기 편해졌다.

/// <summary>
/// 몬스터 대기 상태
/// FixedUpdate : X
/// Transition -> 걷기
/// </summary>
public class NormalMonsterState_Idle : NormalMonsterState
{
    private float waitedTime;
    public NormalMonsterState_Idle(NormalMonsterController _normalMonster) : base(_normalMonster)
    {
        HasPhysics = false;
    }

    public override void Enter()
    {
        m_normalMonster.IsMove = false;
        m_normalMonster.Anim.SetBool("IsMove", m_normalMonster.IsMove);
        m_normalMonster.Anim.Play(m_normalMonster.IDLE_HASH);
        m_normalMonster.Rigid.velocity = Vector2.zero;

        // 방향 전환
        Flip();
        waitedTime = 0;
    }

    public override void Update()
    {
        base.Update();
        waitedTime += Time.deltaTime;
        // 3초 대기 후 걷기로 전환
        if (waitedTime > 3)
        {
            m_normalMonster.Rigid.velocity = Vector2.zero;
            m_normalMonster.StateMach.ChangeState(m_normalMonster.StateMach.StateDic[EState.Walk]);
        }
    }
}

이걸 강의를 통해 구현할 당시에는 강사님이 이 중복 코드를 없앨 방법을 따로 제시해주지 않았었지만, 이런 방식으로 해결할 수 있을 거란 생각을 했다. 결과적으로 성공적이었고 12줄의 코드라도 한 번 더 줄이는 데 도움이 되었다.

2.3 UI 작업과 관련하여

간단한 내용이지만 의외로 헷갈릴 수도 있을 거 같아 짚고 넘어가는 것도 나쁘지 않을 것 같다.

UI와 관련한 작업에서는 활성화/비활성화를 할 때 .enabled를 쓴다.
GameObject는 .SetActive(bool)로 쓰다 보니 왜 이게 안 되지 하고 당황한 편.

그리고 사소하지만 오늘 꽤 골머리 앓았던 것이 다음과 같은 문제이다
비활성화된 오브젝트의 코드는 작동하지 않는다. 주의하자.

이것 때문에 게임 오버와 게임 클리어 UI를 구현하는 데 꽤 골머리를 앓았는데, 아무래도 평상시에는 꺼져 있어야 하는 오브젝트이기 때문이었다.
그래서 결국 인게임 UI와 같이 작업했고, 아래와 같이 게임 일시정지 UI와 게임 종료, 클리어 UI를 한 클래스로 정리했다.

using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class PauseMenuUI : MonoBehaviour
{
    // PauseUI
    [SerializeField] private GameObject m_pausePanel;
    [SerializeField] private Button m_pauseButton;
    [SerializeField] private Button m_resumebutton;
    [SerializeField] private Button[] m_titleButton;

    // GameEndUI
    [SerializeField] private GameObject m_gameOverPanel;
    [SerializeField] private TMP_Text m_gameOverText;
    [SerializeField] private TMP_Text m_gameClearText;
    [SerializeField] private TMP_Text m_scoreText;
    [SerializeField] private Button m_restartButton;

    // 게임 종료 UI를 출력을 띄엄띄엄하게하는 효과용
    // 현재 오류가 있어 추후 반영 예정
    // private Coroutine m_coroutine;

    private void Awake() => Init();

    private void Init()
    {
        m_pauseButton.onClick.AddListener(GamePause);
        m_resumebutton.onClick.AddListener(GameResume);
        for (int i = 0; i < m_titleButton.Length; i++)
        {
            m_titleButton[i].onClick.AddListener(ReturnTitle);
        }
        m_restartButton.onClick.AddListener(GameRestart);
    }

    private void Update()
    {
        if(GameManager.Instance.IsGameOver() || GameManager.Instance.IsGameClear())
        {
            m_gameOverPanel.SetActive(true);
            if (GameManager.Instance.IsGameOver())
            {
                m_gameClearText.enabled = false;
            }
            else if (GameManager.Instance.IsGameClear())
            {
                m_gameOverText.enabled = false;
            }

            int score = GameManager.Instance.GetScore();
            m_scoreText.text = $"Score : {score.ToString("000")}";

            //m_coroutine = StartCoroutine(GameEnd());
        }
    }

    #region PauseMenu

    private void GamePause()
    {
        GameManager.Instance.Pause(true);
    }

    private void GameResume()
    {
        GameManager.Instance.Pause(false);
    }

    #endregion

    #region GameEndMenu

    private void ReturnTitle()
    {
        m_gameOverPanel.SetActive(false);
        AudioManager.Instance.PlayBgm(false);
        GameManager.Instance.Pause(false);
        SceneChanger.Instance.SceneChange(SceneName.TitleScene);
    }

    private void GameRestart()
    {
        /*
        if (m_coroutine != null)
        {
            StopCoroutine(m_coroutine);
            m_coroutine = null;
        }
        */
        m_gameOverPanel.SetActive(false);
        GameManager.Instance.GameStart();
        GameManager.Instance.Pause(false);
        SceneChanger.Instance.SceneChange(SceneName.Stage1Scene);
    }

    #endregion

    // 게임 종료 시 UI를
    // 게임 오버/게임 승리 -> 스코어 -> 버튼 순으로 순차적으로 띄우는 기능
    // 오류가 있어 반영하지 못하고 수정중
    /*
    IEnumerator GameEnd()
    {
        WaitForSeconds delay = new WaitForSeconds(1);

        m_gameOverPanel.SetActive(true);

        if (GameManager.Instance.IsGameOver())
        {
            m_gameOverText.enabled = true;
            m_gameClearText.enabled = false;
        }
        else if (GameManager.Instance.IsGameClear())
        {
            m_gameOverText.enabled = false;
            m_gameClearText.enabled = true;
        }

        yield return delay;

        int score = GameManager.Instance.GetScore();
        m_scoreText.text = $"Score : {score.ToString("000")}";

        yield return delay;

        m_restartButton.enabled = true;
        m_titleButton[1].enabled = true;
    }
    */
}

개인적으로는 두 UI를 분리하고 싶었지만, 이번에는 이렇게 만들고 다음에는 이 두 가지(세 가지인가?) UI를 분리할 수 있는 방법에 대해 생각해보고자 한다.

3. 수정해야 할 점과 과제

3.1 (수정) 게임 일시정지 버튼을 누를 때 플레이어가 공격하는 문제

버그 사항이라고 말하기에는 당연히 벌어질 수밖에 없는 문제기는 한데, 일시 정지를 누르는 버튼도 마우스 왼클릭, 플레이어의 공격도 마우스 왼클릭이다 보니 이와 같은 문제가 발생한다.

일시 정지 버튼을 누르면 플레이어가 공격을 하던 중에 멈춰버리니, 공격 소리가 나면서 이렇게 멈춰버린다.
사소한 문제이긴 하지만, 이건 해결할 방법을 찾으면 좋겠다.

3.2 마지막 맵 구현

이제 거의 마무리 단계이다. 4단계 맵의 작업만이 남았으며 시간이 남으면 더 스테이지를 늘릴 계획이다.

3.3 (도전) 보스 몬스터 구현하기

이제 휴일 포함 2일 정도 남았고 남은 과제는 결국 테스트와 마지막 맵 구현, 그리고 버그 수정만 남았다 보니, 막판 스퍼트로 한 가지 작업만 더 해볼까 싶은 생각이 들었다.
스펠 차지시 속도가 느려지는 기능 구현과 보스 몬스터 구현 중에, 나는 보스 몬스터 구현을 하기로 결정했다.
물론 이 작업이 순탄히 마무리될진 모르겠지만, 우선은 할 수 있는 만큼 해 보려고 한다.

profile
게임 만들러 코딩 공부중

0개의 댓글