Unity - Week 07

이응민·2025년 2월 2일

Unity

목록 보기
7/12

종스크롤 2D 슈팅 게임 만들기(3)

이제 게임의 정보를 UI로 표시하기 위해 게임 화면에 UI들을 그린다.

여기서 체력 칸을 구현할때 열을 맞출때 체력을 나타내는 하트들의 이미지의 부모 이미지 UI에서 Grid Layout Group 컴포넌트를 추가해서 정렬을 깔끔하게 할 수 있다. 그러나 이 컴포넌트는 매 프레임마다 UI의 개수를 인식해서 이미지를 갱신하므로 부하를 유발한다. 그래서 정렬을 한 뒤에 Grid Layout Group 컴포넌트는 삭제를 하는 것이 좋다. 그리고 UI의 변화를 담당하는 UIManager 스크립트를 작성한다. 현재 화면에서 변해야할 부분은 Gem의 개수, Score, Bomb의 개수, 하트의 이미지의 출력이다. 그래서 UIManager 스크립트에서 변화가 생겨야할 UI 오브젝트를 찾아야하는데 유니티 자체적으로는 UI 오브젝트의 자식 오브젝트를 찾을 수 없다. 그래서 재귀적으로 오브젝트의 자식에 접근해서 원하는 UI 오브젝트에 접근하도록 한다.

public static Transform FindChildRecursive(Transform parent, string name)
{
    // Depth-First Search
    foreach (Transform child in parent)
    {
        if (child.name == name)
        {
            return child;
        }

        Transform findTrans = FindChildRecursive(child, name);

        if (findTrans != null)
        {
            return findTrans;
        }
    }
    return null;
}

위 소스 코드가 자식 UI 오브젝트를 찾는 FindChildRecursive 함수이다. 인자로 Transform과 오브젝트의 이름을 전달받는데 Transform이 오브젝트간의 부모-자식 관계를 저장하고 있기 때문이다. 그리고 GameObject와 Transform은 거의 비슷하다. 위 함수에서 foreach문을 통해서 부모 오브젝트의 자식 오브젝트들을 하나하나 찾고자하는 오브젝트의 이름과 비교하면서 찾아보고 같은 것이 있다면 그 오브젝트를 반환한다. 그리고 foreach문 안에서 FindChildRecursive 함수를 다시 호출해서 자식 오브젝트의 자식 오브젝트도 비교한다. 그래서 재귀적으로 부모 오브젝트에서부터 자식 오브젝트를 거치면서 원하는 오브젝트와 일치하는 오브젝트를 반환한다. 그래서 변경되어야할 오브젝트 점수 텍스트, 보석 텍스트, 폭탄 텍스트 오브젝트들을 이런식으로 찾아온다.

private TextMeshProUGUI ScoreText
{
    get
    {
        if (scoreText == null)
        {
            tempTrans = MyUtility.FindChildRecursive(canvasTransform, "ScoreText");
            if (tempTrans != null)
            {
                scoreText = tempTrans.GetComponent<TextMeshProUGUI>();
            }
            else
            {
                Debug.Log("UIManager.cs - ScoreText - failed");
                return null;
            }
        }
        return scoreText;
    }
}
private TextMeshProUGUI GemText
{
    get
    {
        if (gemText == null)
        {
            tempTrans = MyUtility.FindChildRecursive(canvasTransform, "GemText");
            if (tempTrans != null)
            {
                gemText = tempTrans.GetComponent<TextMeshProUGUI>();
            }
            else
            {
                Debug.Log("UIManager.cs - GemText - failed");
                return null;
            }
        }
        return gemText;
    }
}
private TextMeshProUGUI BombText
{
    get
    {
        if (bombText == null)
        {
            tempTrans = MyUtility.FindChildRecursive(canvasTransform, "BombText");
            if (tempTrans != null)
            {
                bombText = tempTrans.GetComponent<TextMeshProUGUI>();
            }
            else
            {
                Debug.Log("UIManager.cs - BombText - failed");
                return null;
            }
        }
        return bombText;
    }
}

그리고 ScoreManager 스크립트에서 UI를 변경하는데 필요한 정보들을 담고 있으므로 ScoreManager 스크립트에서 델리게이트를 선언한다.

// C# (delegate)
public delegate void ScoreChange(int score);
public static event ScoreChange OnChangeScore;
public static event ScoreChange OnChangeGemCount;
public static event ScoreChange OnChangeHP;
public static event ScoreChange OnChangeBombCount;
public static event ScoreChange OnChangePower;

// unity Action (no delegate)
public static event Action OnDiedPlayer;

그래서 점수 변화, Gem 개수의 변화, HP의 변화, Bomb 개수의 변화, 파워의 변화 등을 이벤트로 선언할 수 있다. 그리고 유니티 형식의 델리게이트(Action) OnDiedPlayer을 정의한다. 그리고 데이터를 초기화하는 InitDataReset함수에서 초기값을 정하고 Invoke 함수를 통해 이벤트가 발생했음을 알려준다.

public void InitDataReset()
{
    SetScore = 0;

    curHP = 5;
    maxHP = 5;
    OnChangeHP?.Invoke(curHP);
    powerLevel = 1;
    OnChangePower?.Invoke(powerLevel);
    bombCount = 3;
    if (PlayerPrefs.GetInt(SkillType.Skill_BoomCountAdd.ToString()) > 0)
    {
        bombCount = 5;
    }
    OnChangeBombCount?.Invoke(bombCount);
    gemCount = 0;
    OnChangeGemCount?.Invoke(gemCount);
}

그리고 UIManager의 OnEnable과 OnDisable에서 각각 델리게이트의 이벤트를 받아오고 해제를 해준다.

private void OnEnable()
{
    ScoreManager.OnChangeScore += UpdateScoreText;
    ScoreManager.OnChangeBombCount += UpdateBombText;
    ScoreManager.OnChangeHP += UpdatePlayerHP;
    ScoreManager.OnChangeGemCount += UpdateGemText;
    ScoreManager.OnDiedPlayer += OpenGameOverPopup;
}
private void OnDisable()
{
    ScoreManager.OnChangeScore -= UpdateScoreText;
    ScoreManager.OnChangeBombCount -= UpdateBombText;
    ScoreManager.OnChangeHP -= UpdatePlayerHP;
    ScoreManager.OnChangeGemCount -= UpdateGemText;
    ScoreManager.OnDiedPlayer -= OpenGameOverPopup;
}

그리고 GameManager의 GameStart에서 scoreManager를 초기화한다. 이제 플레이어가 피격되었을 때마다 체력을 감소시키기 위해서 PlayerHitBox 스크립트를 작성해서 Player 오브젝트에 넣어준다.

먼저 RequireComponent를 사용해서 Player에 반드시 필요한 CircleCollider2D와 Rigidbody2D를 설정해주고 Action을 통해서 Player 체력의 변화가 생기는 것을 전달한다. 그리고 TakeDamage 함수에서 체력에 변화가 있다는 것을 전달한다. 다음으로 Bomb을 구현하는데 Bomb은 애니메이션이 두개가 있다. Bomb은 중앙으로 날아가다가 중앙에 도달하게 되면 점점 커지면서 터지게 되는데 흔들거리는 애니메이션과 중앙에 도달해서 점점 크기가 커지는 애니메이션이 있다. 그래서 먼저 Bomb_Idle 애니메이션이 작동되게하고 Transition을 Bomb_Idle에서 Bomb_Fire로 연결하고 Parameter를 설정해서 변화가 생기면 애니메이션을 변화하게 한다. 여기서는 OnFire라는 Trigger를 Parameter로 설정해서 OnFire라는 트리거가 작동되면 애니메이션이 변경된다.

위 스크립트는 폭탄에 대한 PlayerBomb 스크립트이다. 이 스크립트에서는 Player가 어디에 있든 가운데로 이동하는 시간을 1.5초로 정해두고 Lerp를 통해 선형보간해서 서서히 이동해도록 했는데 Lerp의 인자중에 사이 값인 t를 AnimationCurve로 설정해서 빠르게 이동했다가 점점 느리게 이동하게 만들 수 있다.

위 사진은 cruve를 설정해서 t값을 급격하게 증가시켰다가 서서히 1로 다다르게 하는 것을 보여준다. 그래서 Player가 어디에 있든 1.5초에 걸쳐 (0, 0)으로 날아가는데 빠르게 가다가 (0, 0)에 가까워지면 천천히 다가가게 된다. 그리고 Animator를 통해서 애니메이션을 변경할 수 있다. Aminator클래스 변수인 anims를 선언해주고 anims.SetTrigger("OnFire")를 통해서 애니메이션을 Bomb_Fire로 바꾼다. 그리고 일정시간이 지나고 OnFire 함수를 통해서 적에게 데미지를 준다. OnFire 함수는 Enemy 태그를 갖는 캐릭터들을 찾아서 100의 데미지를 준다. 그리고 PlayerWeapon 스크립트에서 폭탄을 생성하는 LaunchBomb 함수를 추가하고 PlayerController 스크립트의 CustomUpdate 함수에서 스페이스바를 눌렀을 때 폭탄이 나가도록 weapon?.LaunchBomb을 추가해준다. 다음에는 보스에 대해서 다루도록 한다. Enemy와 똑같이 이미지를 잘라서 Sprite를 만들어주고 애니메이션을 만들어준다. 그리고 Enemy_Boss 스크립트를 만든다. Enemy_Boss 스크립트에서는 데미지를 받는 기능과 유한 상태 기계(Finite State Machine)을 통해서 보스의 AI를 구현하고 여러가지 보스의 패턴을 구현한다.

먼저 보스의 상태(State)를 enum으로 저장한다. 등장(Appear), Phase01, Phase02 세 개의 state를 갖는다. 그리고 보스의 무기 배열과 현재 무기를 정의하고 보스가 죽었는지 살았는지를 델리게이트로 정의해서 보스가 죽으면 다음 라운드로 넘어가도록 EnemySpawnManager가 이벤트를 받아서 처리하도록한다. 그리고 보스의 AI를 바꿔주는데 코루틴 함수와 state의 이름을 같게 해서 ChangeState 함수에서 새로운 state를 전달받아서 해당 state에 맞는 코루틴 함수를 실행한다. 여기에서 region이라는 전처리기를 사용했는데 region은 코드가 길어질 때 보기 편하게 하기위해서 제목을 붙여서 그룹처럼 함수들을 정리해두는 것이다. 그래서 그 함수들을 볼 필요가 없을 때는 접어둘 수 있다.

0개의 댓글