Week 07 / Period 02 / Lecture & Demo

UI, 소리, 상태를
하나의 흐름으로 묶기

GameManager가 현재 상태를 한 곳에서 관리하고 UI와 소리에 같은 사실을 전달합니다.

중앙의 게임 관리자 장치가 점수 표시기, 수집물, 스피커, 재시작 버튼과 빌드 상자에 케이블로 연결된 입체 모형
GAME MANAGER가 하나의 상태를 관리하고 HUD / AUDIO / RESULT / RESTART를 같은 흐름으로 조정합니다.

학습 목표

2교시의 도착점

교수자의 완성 장면과 코드를 따라 읽습니다. 학생 개별 제작은 3교시에 시작합니다.

  • 01

    Canvas 안의 HUD, Start Panel, Result Panel을 역할별로 연결합니다.

  • 02

    GameManager가 점수, 시간과 상태를 한곳에서 관리하는 코드를 읽습니다.

  • 03

    AudioSource.PlayOneShot과 Button On Click이 상태 함수에 연결되는 과정을 설명합니다.

  • 04

    Scene List를 확인하고 Build Profiles에서 실제 실행 파일을 만드는 흐름을 이해합니다.

수업 흐름

60분의 설명과 시연 흐름

완성된 결과를 먼저 실행한 뒤 Scene, Inspector, Script, Build 순서로 원인을 분해합니다.

00-60 min

30초 게임 루프 완성 시연

Lecture / professor demo

  1. 완성본 먼저 플레이

    Start, 수집, 성공, 재시작, 시간 종료와 실패를 한 번씩 보여 주고 필요한 상태를 회수합니다.

  2. Scene과 Canvas 계층

    GameManager, Canvas, AudioSource와 Collectible의 참조 관계를 설명합니다.

  3. GameManager 코드

    상태 정의, 타이머, 점수, 종료와 UI 갱신 함수를 실행 순서대로 읽습니다.

  4. 수집 사건과 Audio

    Trigger가 AddScore를 호출하고 PlayOneShot과 목표 판정이 이어지는 흐름을 시연합니다.

  5. Button과 재시작

    StartRound와 RestartScene을 On Click에 연결하고 Scene List가 필요한 이유를 확인합니다.

  6. Build와 회귀 테스트

    Build Profiles에서 실행 파일을 만든 뒤 성공, 재시작과 실패를 다시 검증합니다.

전체 구조

사건은 들어오고, 피드백은 나갑니다

GameManager는 모든 것을 직접 감지하지 않습니다. 다른 Component가 사건을 전달하면 규칙을 판단하고 필요한 출력에 결과를 보냅니다.

GameObjectGameManager점수, 시간, 상태와 전체 규칙의 단일 진실
INPUT EVENTStart Button · CollectibleStartRound와 AddScore를 호출
VISUAL OUTPUTHUD · Start Panel · Result Panel현재 데이터와 상태를 화면에 표시
AUDIO OUTPUTAudioSource · AudioClip 3개수집, 성공과 실패를 짧은 소리로 전달
  1. 01 / TRIGGERPlayer 접촉

    Collectible이 Player Tag를 확인합니다.

  2. 02 / CALLAddScore(1)

    GameManager에 수집 사건을 전달합니다.

  3. 03 / UPDATE점수와 HUD

    정수 점수를 올리고 Text를 갱신합니다.

  4. 04 / FEEDBACKPlayOneShot

    수집 효과음을 한 번 재생합니다.

  5. 05 / RULE목표 판정

    3개라면 Won으로 전환합니다.

책임의 경계 Collectible은 “나를 Player가 건드렸다”만 알고, GameManager는 “현재 이 사건을 받아도 되는가, 게임이 끝났는가”를 판단합니다.

씬 계약

코드보다 먼저 연결할 자리를 만듭니다

Script의 필드는 Scene의 실제 Component를 가리켜야 합니다. 이름을 맞추는 것만으로 연결되지는 않으며 Inspector에 참조를 직접 할당합니다.

SceneWeek07_Preflight
Goal3 collectibles
Time30 seconds
ConsoleError 0
  1. 01

    Canvas와 세 레이어

    최상위 Canvas 안에 HUD, StartPanel, ResultPanel을 만듭니다. HUD에는 TextMeshPro UI Text인 ScoreText와 TimerText를, 각 Panel에는 한 개의 UI Button을 둡니다. 처음 TMP 가져오기 창이 보이면 Essential Resources를 한 번 가져옵니다.

    확인 Game View 비율을 바꿔도 HUD가 화면 모서리, Panel이 중앙 기준을 유지합니다.

  2. 02

    GameManager와 AudioSource

    빈 GameObject에 GameManager Script와 AudioSource를 추가합니다. Play On Awake와 Loop는 끄고 수업용 효과음의 Volume을 먼저 낮게 맞춥니다.

    확인 AudioSource가 하나이며 Audio Listener는 Main Camera에 하나만 있습니다.

  3. 03

    GameManager와 Collectible 참조 할당

    GameManager Inspector의 Text, Panel, AudioSource와 AudioClip 필드에 실제 대상을 끌어 놓습니다. 이어 세 Collectible을 함께 선택하고 각 gameManager 필드에 Scene의 GameManager를 할당합니다.

    확인 GameManager와 세 Collectible의 필드가 모두 None이 아니고 Console Error가 0개입니다.

  4. 04

    Scene 저장과 Scene List에 등록

    Assets/Scenes/Week07_Preflight.unity로 저장한 뒤 File → Build Profiles → Platforms → Scene List에서 Add Open Scenes를 누릅니다. RestartScene이 사용하는 buildIndex는 이 목록에서 정해집니다.

    확인 Scene 옆 체크가 켜져 있고 목록에 한 번만 들어 있습니다.

  5. 05

    Button On Click 연결

    StartButton에는 GameManager.StartRound, RestartButton에는 GameManager.RestartScene을 연결합니다.

    확인 Button의 On Click 목록에서 Scene의 GameManager 오브젝트와 올바른 public 함수가 보입니다.

필드연결 대상없을 때 보이는 증상
scoreTextHUD / ScoreText의 TMP_Text점수 갱신 시 NullReferenceException
timerTextHUD / TimerText의 TMP_Text첫 프레임 또는 Update에서 오류
resultTextResultPanel / ResultText의 TMP_Text성공·실패 전환 순간 오류
startPanelCanvas / StartPanelReady 화면을 켜거나 숨길 때 오류
resultPanelCanvas / ResultPanel종료 화면을 표시할 때 오류
sfxSourceGameManager의 AudioSource효과음 재생 시 오류 또는 무음
Collectible.gameManager세 수집물 각각 / Scene의 GameManager첫 수집 접촉에서 NullReferenceException

상태 시연

버튼으로 상태 전환을 먼저 예측합니다

교수자가 버튼을 누르기 전에 다음 상태, 점수와 허용될 버튼을 학생에게 묻습니다. 이 모형은 Unity 코드의 규칙만 단순화해 보여 줍니다.

PROFESSOR CONTROL

Game state simulator

현재 상태에서 허용된 사건만 누를 수 있습니다. 수집 세 번이 먼저 들어오면 Won, 시간 종료가 먼저 들어오면 Lost가 됩니다.

OBSERVABLE OUTPUT
Ready

시작 화면을 보여 주고 게임 시간은 멈춰 있습니다.

score
0 / 3
time
30.0초
state
Ready

현재 Ready입니다. 다음에 허용될 사건을 예측하세요.

게임 매니저

한 Script가 상태와 출력의 순서를 관리합니다

코드를 위에서 아래로 외우지 않습니다. 설정값, 현재 데이터, 매 프레임 규칙, 외부 사건, 상태 전환, 화면 출력 순서로 책임을 나눠 읽습니다.

RULEStargetScore · roundDuration

Inspector에서 바꾸는 게임 규칙입니다.

STATEscore · timeRemaining

실행 중 변하는 현재 데이터입니다.

EVENTSStartRound · AddScore

Button과 Trigger가 호출하는 public 함수입니다.

OUTPUTUpdateHud · PlaySfx

데이터를 보이는 글자와 소리로 바꿉니다.

GameManager.cs · instructor reference
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

public enum GameState
{
    Ready,
    Playing,
    Won,
    Lost
}

public class GameManager : MonoBehaviour
{
    [Header("Rules")]
    [SerializeField, Min(1)] private int targetScore = 3;
    [SerializeField, Min(1f)] private float roundDuration = 30f;

    [Header("UI")]
    [SerializeField] private TMP_Text scoreText;
    [SerializeField] private TMP_Text timerText;
    [SerializeField] private TMP_Text resultText;
    [SerializeField] private GameObject startPanel;
    [SerializeField] private GameObject resultPanel;

    [Header("Audio")]
    [SerializeField] private AudioSource sfxSource;
    [SerializeField] private AudioClip collectClip;
    [SerializeField] private AudioClip winClip;
    [SerializeField] private AudioClip loseClip;

    public GameState CurrentState { get; private set; }

    private int score;
    private float timeRemaining;

    private void Start()
    {
        ShowReady();
    }

    private void Update()
    {
        if (CurrentState != GameState.Playing)
        {
            return;
        }

        timeRemaining = Mathf.Max(
            0f,
            timeRemaining - Time.deltaTime
        );
        UpdateHud();

        if (timeRemaining <= 0f)
        {
            FinishRound(GameState.Lost);
        }
    }

    private void ShowReady()
    {
        CurrentState = GameState.Ready;
        score = 0;
        timeRemaining = roundDuration;
        Time.timeScale = 0f;

        startPanel.SetActive(true);
        resultPanel.SetActive(false);
        UpdateHud();
    }

    public void StartRound()
    {
        if (CurrentState != GameState.Ready)
        {
            return;
        }

        CurrentState = GameState.Playing;
        Time.timeScale = 1f;

        startPanel.SetActive(false);
        resultPanel.SetActive(false);
        UpdateHud();
    }

    public void AddScore(int amount)
    {
        if (CurrentState != GameState.Playing)
        {
            return;
        }

        score = Mathf.Min(score + amount, targetScore);
        PlaySfx(collectClip);
        UpdateHud();

        if (score >= targetScore)
        {
            FinishRound(GameState.Won);
        }
    }

    private void FinishRound(GameState result)
    {
        if (CurrentState != GameState.Playing)
        {
            return;
        }

        CurrentState = result;
        Time.timeScale = 0f;
        resultPanel.SetActive(true);

        bool didWin = result == GameState.Won;
        resultText.text = didWin ? "성공" : "시간 종료";
        PlaySfx(didWin ? winClip : loseClip);
    }

    public void RestartScene()
    {
        Time.timeScale = 1f;
        int currentScene = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentScene);
    }

    private void UpdateHud()
    {
        scoreText.text = $"{score} / {targetScore}";
        timerText.text = $"{timeRemaining:0.0}";
    }

    private void PlaySfx(AudioClip clip)
    {
        if (clip != null)
        {
            sfxSource.PlayOneShot(clip);
        }
    }
}

CurrentState != Playing

종료 뒤 추가 수집과 중복 종료를 막는 가장 앞쪽의 문입니다.

Mathf.Max(0f, ...)

남은 시간이 음수로 표시되지 않게 0에서 고정합니다.

Time.timeScale

Ready와 결과 상태에서 게임 시간을 0으로 멈추고 Start에서 1로 복원합니다.

UpdateHud()

점수나 시간이 바뀐 직후 같은 함수로 화면 표현을 다시 씁니다.

SceneManager.LoadScene

현재 Scene을 다시 불러와 수집물과 모든 실행 데이터를 초기 상태로 돌립니다.

수집 사건

수집물은 사건을 한 번만 전달합니다

접촉 감지와 전체 게임 규칙을 분리합니다. Collectible은 올바른 Player가 닿았는지 확인하고 GameManager에 점수 사건을 전달한 뒤 자신을 숨깁니다.

Collectible.cs · event sender
using UnityEngine;

public class Collectible : MonoBehaviour
{
    [SerializeField] private GameManager gameManager;

    private bool collected;

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (collected || !other.CompareTag("Player"))
        {
            return;
        }

        if (gameManager.CurrentState != GameState.Playing)
        {
            return;
        }

        collected = true;
        gameManager.AddScore(1);
        gameObject.SetActive(false);
    }
}

검사통과할 때통과하지 못할 때
collected아직 처리하지 않은 수집물같은 사건의 중복 점수를 막고 return
CompareTag("Player")Player가 접촉다른 Collider 접촉은 무시
CurrentStatePlaying에서만 점수 허용Ready, Won, Lost에서는 무시
정확히 한 번 collected = true를 먼저 기록하고 점수를 전달합니다. 결과가 Won으로 바뀌어도 현재 수집 사건의 후속 처리는 예측 가능한 순서로 끝납니다.

오디오 피드백

소리는 사건의 차이를 즉시 알려 줍니다

효과음은 장면을 꾸미는 배경이 아니라 입력과 결과 사이의 짧은 확인 신호입니다. 세 소리는 길이와 역할을 분명히 다르게 합니다.

COLLECT

짧고 가벼운 확인

수집 직후 한 번 들립니다. 반복해서 들어도 피곤하지 않게 가장 짧고 작은 음량으로 시작합니다.

WIN

올라가는 완료 신호

목표 달성 뒤 한 번만 들립니다. Collect보다 길고 분명하지만 결과 Text를 가리지 않습니다.

LOSE

낮고 짧은 종료 신호

시간 종료 뒤 한 번만 들립니다. 실패를 알리되 불쾌하게 크거나 길지 않게 조절합니다.

MIX

작은 음량에서 시작

AudioSource Volume과 Clip 자체 음량을 구분합니다. 교실 스피커 기준으로 작게 시작해 필요한 만큼만 올립니다.

Play()

AudioSource의 기본 Clip 재생

AudioSource의 Clip 필드에 지정된 소리를 재생합니다. 같은 Source에서 다시 Play하면 재생 흐름이 바뀔 수 있습니다.

PlayOneShot(clip)

전달한 Clip을 한 번 재생

짧은 효과음에 적합합니다. 이미 PlayOneShot으로 재생 중인 Clip을 취소하지 않아 연속 수집음도 겹쳐 들릴 수 있습니다.

빌드 프로파일

Editor 밖에서 다시 검증해야 완성입니다

Play Mode는 개발 환경 안의 확인입니다. 중간고사 전 사전 점검에서는 실제 빌드를 열어 Scene, 입력, UI, Audio와 재시작을 다시 확인합니다.

  1. 01

    Scene 저장

    Assets/Scenes/Week07_Preflight.unity의 최신 변경을 저장하고 Play Mode를 종료합니다. 저장하지 않은 변경 표시가 없는지 확인합니다.

    확인 Project 창에서 Scene 파일을 다시 선택할 수 있습니다.

  2. 02

    Scene List 재확인

    File → Build Profiles → Platforms → Scene List에서 앞서 등록한 Scene과 체크 상태를 다시 확인합니다. 빠져 있다면 Add Open Scenes로 추가합니다.

    확인 Scene 옆 체크가 켜져 있고 목록에 한 번만 들어 있습니다.

  3. 03

    활성 프로파일 확인

    수업 컴퓨터에 설치된 데스크톱 플랫폼을 선택합니다. 별도 프로파일을 쓴다면 Active 표시와 Scene List를 다시 확인합니다.

    확인 Build와 Build and Run 버튼이 활성 프로파일에 보입니다.

  4. 04

    Build and Run

    프로젝트 밖의 Builds/Week07 폴더를 선택해 빌드하고 실행합니다. 첫 빌드는 이후 증분 빌드보다 오래 걸릴 수 있습니다.

    확인 실행 파일이 열리고 Start 화면부터 시작합니다.

  5. 05

    빌드 회귀 테스트

    Start → Won → Restart → Start → Lost → Restart 순서로 실행합니다. Restart 뒤에는 Ready이므로, 실패 라운드를 시작하려면 Start를 다시 눌러야 합니다. Editor에서만 작동하고 빌드에서 실패하면 Scene List와 Inspector 참조부터 확인합니다.

    확인 두 라운드를 연속 실행해도 점수와 수집물이 모두 초기화되고 마지막에는 Ready로 돌아옵니다.

  1. SCENE

    저장 완료

  2. LIST

    Scene 포함

  3. PROFILE

    Active 확인

  4. BUILD

    실행 파일 생성

  5. TEST

    성공·실패·재시작

예측과 설명

코드보다 먼저 실행 결과를 말합니다

각 질문은 교수자 시연 직전에 사용합니다. 학생의 예측을 들은 뒤 한 줄만 바꾸거나 참조 하나만 빼서 결과를 확인합니다.

Q1. Result Panel 참조가 None이면 언제 오류가 납니까?

ShowReady에서 resultPanel.SetActive(false)를 호출하므로 Scene 시작 직후 오류가 납니다. 오류가 성공 순간에만 생긴다고 단정하면 호출 순서를 놓친 것입니다.

Q2. Won 뒤 수집물에 닿으면 점수가 오릅니까?

오르지 않습니다. Collectible과 AddScore가 모두 Playing인지 검사하고, Won에서는 곧바로 return합니다.

Q3. RestartScene 전에 Time Scale을 1로 돌리는 이유는?

Time.timeScale은 Scene을 다시 불러와도 전역 값으로 남을 수 있습니다. 먼저 1로 복원해야 새 Scene의 초기화가 멈춘 시간 위에서 시작하지 않습니다.

Q4. Editor에서는 재시작되는데 빌드에서 실패한다면?

현재 Scene이 Build Profiles의 Scene List에 포함되어 있고 체크가 켜져 있는지 먼저 확인합니다. LoadScene이 사용하는 buildIndex는 빌드 씬 목록과 연결됩니다.

01

외부 사건을 받는 public 함수는 StartRound, ______과 RestartScene입니다.

02

결과 상태에서 중복 처리를 막는 첫 조건은 CurrentState != ______입니다.

03

현재 Scene을 buildIndex로 다시 열려면 그 Scene이 Build Profiles의 ______에 포함되어야 합니다.

공식 자료

Unity 공식 자료

Audio 재생, 시간 정지, Scene 재로드와 Unity 6 Build Profiles 절차를 공식 문서로 다시 확인할 수 있습니다.