[Unit] Game UI and game loop > [Tutorial] Close the core game loop

  1. 에셋 및 구조 생성
  • 프로젝트(Project): 우클릭 → Create → UI Toolkit → UI Document 생성 (이름: EndScreen)
  • UI 빌더(UI Builder): EndScreen 더블 클릭 → VisualElement 생성 (LoseScreenContainer) → 그 자식으로 VisualElement 하나 더 생성 (LoseScreen)
    1. 배경 및 애니메이션 설정 (Container)
  • Hierarchy: LoseScreenContainer 선택
  • Opacity: 0 (평소엔 투명하게)
  • Position: Absolute 변경 & Top/Left/Right/Bottom: 모두 0 (전체 화면)
  • Background: Color를 Black, Alpha를 255로 설정
  • Transition Animations: Property: opacity, Duration: 1s, Easing: ease

❓Transition Animations이 무엇인가요?

  • Property (opacity): “투명도”를 조절해서 페이드 효과를 줌
  • Duration (1s): 효과가 완성되는 데 “1초”를 씀
  • Easing (ease): 처음과 끝은 느리고 중간은 빠르게 하여 자연스러운 속도감을 줌
  1. 승패 이미지 설정 (Screen)
  • Hierarchy: LoseScreen 선택
  • Background: Image 유형을 Sprite로 변경 → LoseScreen 스프라이트 드래그
  • Size Mode: Scale to fit (이미지 비율 유지)
  • 반복: 위 과정을 반복해 WinScreenContainer와 하위 요소도 생성
    1. 최종 조립
  • UI 빌더: GameUI 에셋 열기
  • Library: EndScreen을 GameUI 하위로 드래그 (인스턴스 추가)
  • Inspector: 추가된 EndScreen 선택 → Position: Absolute & 모든 앵커(0) 설정

게임 매니저

using Beginner2D;
using UnityEngine;
using UnityEngine.SceneManagement; // 씬(레벨) 재시작을 위해 필요한 네임스페이스

public class GameManager : MonoBehaviour
{
    public PlayerController player; // 플레이어 참조 (체력 확인용)
    EnemyController[] enemies;      // 맵에 있는 모든 적을 저장할 배열
    public UIHandler uiHandler;     // UI 제어 스크립트 참조 (승패 화면 표시용)

    void Start()
    {
        // 게임 시작 시 씬에 배치된 모든 EnemyController를 찾아 배열에 저장
        // FindObjectsSortMode.None는 정렬 옵션 없음
        enemies = FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
    }

    void Update()
    {
        // 1. 패배 조건: 플레이어 체력이 0 이하인가?
        if (player.health <= 0)
        {
            uiHandler.DisplayLoseScreen(); // 패배 UI 출력 (아까 만든 페이드 효과 발동)
            Invoke(nameof(ReloadScene), 3f); // 3초 뒤에 ReloadScene 함수 실행
        }

        // 2. 승리 조건: 모든 적이 고쳐졌는가?
        if (AllEnemiesFixed())
        {
            uiHandler.DisplayWinScreen();  // 승리 UI 출력
            Invoke(nameof(ReloadScene), 3f); // 3초 뒤에 ReloadScene 함수 실행
        }
    }

    // 모든 적의 상태를 체크하는 함수
    bool AllEnemiesFixed()
    {
        foreach (EnemyController enemy in enemies)
        {
            // 한 명이라도 여전히 고장(isBroken) 상태라면 false 반환
            if (enemy.isBroken) return false;
        }
        // 모든 적을 확인했는데 고장 난 적이 없다면 true 반환
        return true;
    }

    // 현재 씬을 다시 불러오는 함수 (게임 재시작)
    // SceneManager.GetActiveScene().name 현재 내가 플레이 중인 씬의 이름을 알아냄
    void ReloadScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

🔍 nameof(ReloadScene)

함수 이름을 문자열(“ReloadScene”)로 써도 되지만, nameof를 쓰면 나중에 함수 이름을 바꿔도 에러를 바로 잡아줘서 훨씬 안전합니다.

UI

using UnityEngine;
using UnityEngine.UIElements; // UI Toolkit 기능을 사용하기 위해 필요한 네임스페이스

public class UIHandler : MonoBehaviour
{
    
    private VisualElement m_Healthbar; // UI의 개별 요소(VisualElement)를 담을 변수
    public static UIHandler instance { get; private set; }

    // UI dialogue window variables
    public float displayTime = 4.0f; // 대화창이 떠 있을 시간
    private VisualElement m_NonPlayerDialogue; // NPC 대화창 UI 요소
    private float m_TimerDisplay; // 남은 표시 시간을 체크할 타이머

    // 승패 장면
    private VisualElement m_WinScreen;
    private VisualElement m_LoseScreen;

    // Awake is called when the script instance is being loaded (in this situation, when the game scene loads)
    private void Awake()
    {
        instance = this;
    }

    // 객체가 생성된 후 첫 번째 Update 직전에 호출되는 함수
    void Start()
    {
        // 1. 현재 오브젝트에 붙어있는 UIDocument 컴포넌트를 가져옵니다.
        UIDocument uiDocument = GetComponent<UIDocument>();

        // 2. UI 레이아웃(UXML)에서 이름이 "HealthBar"인 요소를 찾아 변수에 할당합니다.
        // Q는 'Query'의 약자로, 특정 요소를 찾는 기능을 합니다.
        m_Healthbar = uiDocument.rootVisualElement.Q<VisualElement>("HealthBar");

        // 3. 시작할 때 체력바를 100%(1.0)로 초기화합니다.
        SetHealthValue(1.0f);

        // 이름이 "NPCDialogue"인 요소를 찾고, 처음에는 화면에서 숨김
        m_NonPlayerDialogue = uiDocument.rootVisualElement.Q<VisualElement>("NPCDialogue");
        m_NonPlayerDialogue.style.display = DisplayStyle.None;
        m_TimerDisplay = -1.0f; // 타이머 초기화 (-1은 작동하지 않는 상태를 의미)

        m_LoseScreen = uiDocument.rootVisualElement.Q<VisualElement>("LoseScreenContainer");
        m_WinScreen = uiDocument.rootVisualElement.Q<VisualElement>("WinScreenContainer");
    }

    // 외부(예: Player 스크립트)에서 체력 수치를 변경할 때 호출하는 함수
    public void SetHealthValue(float percentage)
    {
        // m_Healthbar의 가로 길이(width) 스타일을 퍼센트 단위로 변경합니다.
        // 0.0 ~ 1.0 사이의 값을 받아 0% ~ 100%로 변환하여 적용합니다.
        // 퍼센트는 부모 너비 기준으로 작동합니다.
        m_Healthbar.style.width = Length.Percent(100 * percentage);
    }

    private void Update()
    {
        if (m_TimerDisplay > 0)
        {
            m_TimerDisplay -= Time.deltaTime;

            // 시간이 다 되면 대화창을 다시 숨김
            if (m_TimerDisplay < 0)
            {
                m_NonPlayerDialogue.style.display = DisplayStyle.None;
            }
        }
    }

    public void DisplayDialogue()
    {
        m_NonPlayerDialogue.style.display = DisplayStyle.Flex; // 대화창 보이기
        m_TimerDisplay = displayTime; // 타이머 리셋
    }

    public void DisplayWinScreen()
    {
        m_WinScreen.style.opacity = 1.0f;
    }

    public void DisplayLoseScreen()
    {
        m_LoseScreen.style.opacity = 1.0f;
    }

}

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public bool isBroken { get { return broken; } } // 추가

    // Public variables
    public float speed;
    public bool vertical;
    public float changeTime = 3.0f;

    // Private variables
    Rigidbody2D rigidbody2d;
    Animator animator;
    float timer;
    int direction = 1;
    bool broken = true;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        timer = changeTime;
    }

    // Update is called every frame
    void Update()
    {
        timer -= Time.deltaTime;

        if (timer < 0)
        {
            direction = -direction;
            timer = changeTime;
        }
    }

    // FixedUpdate has the same call rate as the physics system
    void FixedUpdate()
    {
        if (!broken)
        {
            return;
        }

        Vector2 position = rigidbody2d.position;

        if (vertical)
        {
            position.y = position.y + speed * direction * Time.deltaTime;
            animator.SetFloat("Move X", 0); // "Move X"라는 변수 값을 0으로 만듦
            animator.SetFloat("Move Y", direction);
        }
        else
        {
            position.x = position.x + speed * direction * Time.deltaTime;
            animator.SetFloat("Move X", direction);
            animator.SetFloat("Move Y", 0);
        }
        
        rigidbody2d.MovePosition(position);
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        PlayerController player = other.gameObject.GetComponent<PlayerController>();

        if (player != null)
        {
            player.ChangeHealth(-1);
        }
    }

    public void Fix()
    {
        broken = false;
        // 적 게임 오브젝트를 물리 시스템의 충돌 시뮬레이션에서 제외.
        // 투사체가 더 이상 적과 충돌하지 않으며 적 캐릭터에게 피해를 못 주게 됨.
        rigidbody2d.simulated = false;
        animator.SetTrigger("Fixed");
    }
}

📌 출처: [Unity Learn] 2D Adventure: Robot Repair