학습 목표
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 순서로 원인을 분해합니다.
30초 게임 루프 완성 시연
Lecture / professor demo
- 완성본 먼저 플레이
Start, 수집, 성공, 재시작, 시간 종료와 실패를 한 번씩 보여 주고 필요한 상태를 회수합니다.
- Scene과 Canvas 계층
GameManager, Canvas, AudioSource와 Collectible의 참조 관계를 설명합니다.
- GameManager 코드
상태 정의, 타이머, 점수, 종료와 UI 갱신 함수를 실행 순서대로 읽습니다.
- 수집 사건과 Audio
Trigger가 AddScore를 호출하고 PlayOneShot과 목표 판정이 이어지는 흐름을 시연합니다.
- Button과 재시작
StartRound와 RestartScene을 On Click에 연결하고 Scene List가 필요한 이유를 확인합니다.
- Build와 회귀 테스트
Build Profiles에서 실행 파일을 만든 뒤 성공, 재시작과 실패를 다시 검증합니다.
전체 구조
사건은 들어오고, 피드백은 나갑니다
GameManager는 모든 것을 직접 감지하지 않습니다. 다른 Component가 사건을 전달하면 규칙을 판단하고 필요한 출력에 결과를 보냅니다.
- 01 / TRIGGERPlayer 접촉
Collectible이 Player Tag를 확인합니다. - 02 / CALLAddScore(1)
GameManager에 수집 사건을 전달합니다.
- 03 / UPDATE점수와 HUD
정수 점수를 올리고 Text를 갱신합니다.
- 04 / FEEDBACKPlayOneShot
수집 효과음을 한 번 재생합니다.
- 05 / RULE목표 판정
3개라면 Won으로 전환합니다.
씬 계약
코드보다 먼저 연결할 자리를 만듭니다
Script의 필드는 Scene의 실제 Component를 가리켜야 합니다. 이름을 맞추는 것만으로 연결되지는 않으며 Inspector에 참조를 직접 할당합니다.
- 01
Canvas와 세 레이어
최상위 Canvas 안에
HUD,StartPanel,ResultPanel을 만듭니다. HUD에는 TextMeshPro UI Text인 ScoreText와 TimerText를, 각 Panel에는 한 개의 UI Button을 둡니다. 처음 TMP 가져오기 창이 보이면 Essential Resources를 한 번 가져옵니다.확인 Game View 비율을 바꿔도 HUD가 화면 모서리, Panel이 중앙 기준을 유지합니다.
- 02
GameManager와 AudioSource
빈 GameObject에
GameManagerScript와 AudioSource를 추가합니다. Play On Awake와 Loop는 끄고 수업용 효과음의 Volume을 먼저 낮게 맞춥니다.확인 AudioSource가 하나이며 Audio Listener는 Main Camera에 하나만 있습니다.
- 03
GameManager와 Collectible 참조 할당
GameManager Inspector의 Text, Panel, AudioSource와 AudioClip 필드에 실제 대상을 끌어 놓습니다. 이어 세 Collectible을 함께 선택하고 각
gameManager필드에 Scene의 GameManager를 할당합니다.확인 GameManager와 세 Collectible의 필드가 모두 None이 아니고 Console Error가 0개입니다.
- 04
Scene 저장과 Scene List에 등록
Assets/Scenes/Week07_Preflight.unity로 저장한 뒤File → Build Profiles → Platforms → Scene List에서 Add Open Scenes를 누릅니다.RestartScene이 사용하는 buildIndex는 이 목록에서 정해집니다.확인 Scene 옆 체크가 켜져 있고 목록에 한 번만 들어 있습니다.
- 05
Button On Click 연결
StartButton에는
GameManager.StartRound, RestartButton에는GameManager.RestartScene을 연결합니다.확인 Button의 On Click 목록에서 Scene의 GameManager 오브젝트와 올바른 public 함수가 보입니다.
| 필드 | 연결 대상 | 없을 때 보이는 증상 |
|---|---|---|
scoreText | HUD / ScoreText의 TMP_Text | 점수 갱신 시 NullReferenceException |
timerText | HUD / TimerText의 TMP_Text | 첫 프레임 또는 Update에서 오류 |
resultText | ResultPanel / ResultText의 TMP_Text | 성공·실패 전환 순간 오류 |
startPanel | Canvas / StartPanel | Ready 화면을 켜거나 숨길 때 오류 |
resultPanel | Canvas / ResultPanel | 종료 화면을 표시할 때 오류 |
sfxSource | GameManager의 AudioSource | 효과음 재생 시 오류 또는 무음 |
Collectible.gameManager | 세 수집물 각각 / Scene의 GameManager | 첫 수집 접촉에서 NullReferenceException |
상태 시연
버튼으로 상태 전환을 먼저 예측합니다
교수자가 버튼을 누르기 전에 다음 상태, 점수와 허용될 버튼을 학생에게 묻습니다. 이 모형은 Unity 코드의 규칙만 단순화해 보여 줍니다.
Game state simulator
현재 상태에서 허용된 사건만 누를 수 있습니다. 수집 세 번이 먼저 들어오면 Won, 시간 종료가 먼저 들어오면 Lost가 됩니다.
시작 화면을 보여 주고 게임 시간은 멈춰 있습니다.
- score
- 0 / 3
- time
- 30.0초
- state
- Ready
현재 Ready입니다. 다음에 허용될 사건을 예측하세요.
게임 매니저
한 Script가 상태와 출력의 순서를 관리합니다
코드를 위에서 아래로 외우지 않습니다. 설정값, 현재 데이터, 매 프레임 규칙, 외부 사건, 상태 전환, 화면 출력 순서로 책임을 나눠 읽습니다.
targetScore · roundDurationInspector에서 바꾸는 게임 규칙입니다.
score · timeRemaining실행 중 변하는 현재 데이터입니다.
StartRound · AddScoreButton과 Trigger가 호출하는 public 함수입니다.
UpdateHud · PlaySfx데이터를 보이는 글자와 소리로 바꿉니다.
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.timeScaleReady와 결과 상태에서 게임 시간을 0으로 멈추고 Start에서 1로 복원합니다.
UpdateHud()점수나 시간이 바뀐 직후 같은 함수로 화면 표현을 다시 씁니다.
SceneManager.LoadScene현재 Scene을 다시 불러와 수집물과 모든 실행 데이터를 초기 상태로 돌립니다.
수집 사건
수집물은 사건을 한 번만 전달합니다
접촉 감지와 전체 게임 규칙을 분리합니다. Collectible은 올바른 Player가 닿았는지 확인하고 GameManager에 점수 사건을 전달한 뒤 자신을 숨깁니다.
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 접촉은 무시 |
CurrentState | Playing에서만 점수 허용 | Ready, Won, Lost에서는 무시 |
오디오 피드백
소리는 사건의 차이를 즉시 알려 줍니다
효과음은 장면을 꾸미는 배경이 아니라 입력과 결과 사이의 짧은 확인 신호입니다. 세 소리는 길이와 역할을 분명히 다르게 합니다.
짧고 가벼운 확인
수집 직후 한 번 들립니다. 반복해서 들어도 피곤하지 않게 가장 짧고 작은 음량으로 시작합니다.
올라가는 완료 신호
목표 달성 뒤 한 번만 들립니다. Collect보다 길고 분명하지만 결과 Text를 가리지 않습니다.
낮고 짧은 종료 신호
시간 종료 뒤 한 번만 들립니다. 실패를 알리되 불쾌하게 크거나 길지 않게 조절합니다.
작은 음량에서 시작
AudioSource Volume과 Clip 자체 음량을 구분합니다. 교실 스피커 기준으로 작게 시작해 필요한 만큼만 올립니다.
Play()AudioSource의 기본 Clip 재생
AudioSource의 Clip 필드에 지정된 소리를 재생합니다. 같은 Source에서 다시 Play하면 재생 흐름이 바뀔 수 있습니다.
PlayOneShot(clip)전달한 Clip을 한 번 재생
짧은 효과음에 적합합니다. 이미 PlayOneShot으로 재생 중인 Clip을 취소하지 않아 연속 수집음도 겹쳐 들릴 수 있습니다.
빌드 프로파일
Editor 밖에서 다시 검증해야 완성입니다
Play Mode는 개발 환경 안의 확인입니다. 중간고사 전 사전 점검에서는 실제 빌드를 열어 Scene, 입력, UI, Audio와 재시작을 다시 확인합니다.
- 01
Scene 저장
Assets/Scenes/Week07_Preflight.unity의 최신 변경을 저장하고 Play Mode를 종료합니다. 저장하지 않은 변경 표시가 없는지 확인합니다.확인 Project 창에서 Scene 파일을 다시 선택할 수 있습니다.
- 02
Scene List 재확인
File → Build Profiles → Platforms → Scene List에서 앞서 등록한 Scene과 체크 상태를 다시 확인합니다. 빠져 있다면 Add Open Scenes로 추가합니다.확인 Scene 옆 체크가 켜져 있고 목록에 한 번만 들어 있습니다.
- 03
활성 프로파일 확인
수업 컴퓨터에 설치된 데스크톱 플랫폼을 선택합니다. 별도 프로파일을 쓴다면 Active 표시와 Scene List를 다시 확인합니다.
확인 Build와 Build and Run 버튼이 활성 프로파일에 보입니다.
- 04
Build and Run
프로젝트 밖의
Builds/Week07폴더를 선택해 빌드하고 실행합니다. 첫 빌드는 이후 증분 빌드보다 오래 걸릴 수 있습니다.확인 실행 파일이 열리고 Start 화면부터 시작합니다.
- 05
빌드 회귀 테스트
Start → Won → Restart → Start → Lost → Restart순서로 실행합니다. Restart 뒤에는 Ready이므로, 실패 라운드를 시작하려면 Start를 다시 눌러야 합니다. Editor에서만 작동하고 빌드에서 실패하면 Scene List와 Inspector 참조부터 확인합니다.확인 두 라운드를 연속 실행해도 점수와 수집물이 모두 초기화되고 마지막에는 Ready로 돌아옵니다.
- SCENE
저장 완료
- LIST
Scene 포함
- PROFILE
Active 확인
- BUILD
실행 파일 생성
- 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는 빌드 씬 목록과 연결됩니다.
외부 사건을 받는 public 함수는 StartRound, ______과 RestartScene입니다.
결과 상태에서 중복 처리를 막는 첫 조건은 CurrentState != ______입니다.
현재 Scene을 buildIndex로 다시 열려면 그 Scene이 Build Profiles의 ______에 포함되어야 합니다.
공식 자료
Unity 공식 자료
Audio 재생, 시간 정지, Scene 재로드와 Unity 6 Build Profiles 절차를 공식 문서로 다시 확인할 수 있습니다.
- AudioSource.PlayOneShot ↗Unity 6.6에서 AudioClip을 한 번 재생하고 기존 One Shot을 유지하는 동작
- 게임 내 시간 및 실시간 ↗Time.timeScale 0의 의미와 UI가 계속 상호작용할 수 있는 이유
- SceneManager.LoadScene ↗Scene 이름 또는 buildIndex로 Scene을 로드하는 함수
- 빌드 프로파일 소개 ↗Unity 6.6에서 플랫폼별 Build Profiles를 관리하는 방식
- 빌드의 씬 관리 ↗Scene List에 Scene을 추가, 제외, 제거하고 순서를 바꾸는 절차