Week 05 / Period 02 / State and interaction

수집·위험·목표와 게임 규칙

보석을 얻은 기록은 어디에 남을까요? 보석 하나를 수집하며 Player에 개수를 기록하고, 위험에 닿았을 때의 복귀와 목표에 도착했을 때의 성공 판정으로 이어 갑니다.

50분 / 상태와 코드 흐름 / 수집 → 복귀 → 조건부 성공

초록 모자 모험가가 보석을 얻고 세 칸 중 한 칸이 켜진 기록 장치 곁에서 위험 구역과 열린 목표 아치로 이어 가는 입체 장면
개념 그림 · 보석이 사라져도 기록은 남습니다. 실습의 개수와 성공 결과는 Console에서 확인합니다.
이 페이지에서
핵심 질문
사라진 보석의 수집 기록은 어디에 남길까요?
학습 방식
스크립트 작성 → 연결 → 실행 확인
이전 학습
Trigger · Tag · 조건문 · 비활성화
이어 쓸 장면
Week05_MicroLevel
수업 시간
핵심 설명과 시연 50분
다음 교시
개인 실습 · 경로 구성 · 검증
학습 목표보석을 얻은 기록을 Player에 남기고, 그 기록으로 복귀와 성공 규칙을 설명합니다.

1교시에는 보석에 닿으면 보석이 사라졌습니다. 이제 그 짧은 접촉이 게임의 기억으로 남아야 합니다. 먼저 Player에 개수를 저장할 스크립트를 만들고, 보석이 그 값을 늘리도록 연결합니다. 이어서 위험 복귀와 목표 판정을 하나씩 작성합니다.

2교시는 교수자의 작성·연결 시연을 따라 PlayerProgress → Collectible → HazardZone → GoalZone 순서로 네 스크립트를 완성합니다. 각 단계에서 코드를 작성하고 저장한 뒤 Unity에서 연결과 실행 결과를 확인합니다. 3교시에는 이 코드로 자신의 경로를 구성합니다.

실습 환경 및 스크립트 준비

수업 프로젝트 /Users/creativeengineer/Desktop/GameEngineI에서 Assets/Scenes/Week05_MicroLevel.unity를 엽니다. 1교시에서 저장한 장면을 사용합니다. 캐릭터, PlayerController, Move·Jump 입력과 기존 Ground·발판을 그대로 이어 갑니다.

연결 전에 확인할 시작 상태
대상확인할 상태
ScenePlay 종료 · Week05_MicroLevel 열기
Player기존 이동·점프 정상 · Tag Player · Rigidbody2D와 Collider2D 유지
Collectible1교시의 보석 하나 · Is Trigger 켬 · Collectible 컴포넌트 하나
GuideZone1교시에 만들었다면 비활성화
Console컴파일 오류 0개

파일을 만드는 공통 순서

  1. 기존 파일부터 확인

    Play를 종료하고 Project 창의 Assets 전체에서 해당 스크립트를 검색합니다. 1교시의 Collectible.cs는 기존 파일을 열어 이어서 수정합니다. 다른 스크립트도 이미 있다면 그 파일을 사용하고, 같은 이름의 class를 새로 만들지 않습니다.

    확인기존 스크립트와 .meta, 오브젝트의 연결을 유지합니다.
  2. 없는 스크립트는 Unity에서 직접 생성

    Project에서 Assets/Scripts 폴더를 선택합니다. 폴더가 없으면 먼저 만듭니다. Create → Scripting → MonoBehaviour Script로 생성하고 해당 단계의 이름을 입력합니다. 예를 들어 PlayerProgress로 만들면 파일은 PlayerProgress.cs, class 이름은 PlayerProgress입니다.

    확인파일 이름과 class 이름의 철자·대소문자를 맞춥니다.
  3. 한 단계씩 작성하고 저장

    파일을 두 번 클릭해 코드 편집기로 엽니다. 각 단계에 보이는 추가·수정할 코드를 지정한 위치에 작성합니다. 바로 아래 적용 후 전체 코드를 펼치면 기존 코드와 합쳐진 파일 모습을 비교할 수 있습니다. 저장 → Unity로 복귀 → 컴파일 오류 0개 확인 → 컴포넌트 연결 순서로 진행합니다.

    확인스크립트는 아래 해당 단계에서 하나씩 작성합니다.

PlayerProgress는 수집 → 시작 위치 저장 → 복귀 → 목표 조건 순서로 완성합니다. 단계마다 추가·수정할 코드는 바로 보이고, 적용 후 전체 코드만 펼쳐 확인할 수 있습니다. 이미 이번 수업의 코드를 완성했다면 기존 파일을 유지하고 각 단계의 구성과 실행 결과를 확인합니다. 완성한 뒤에는 최종 코드 확인에서 전체 구성을 비교할 수 있습니다.

준비 완료

1교시 Scene과 기존 Collectible.cs의 위치를 확인했습니다. 이제 첫 번째 스크립트 PlayerProgress를 만듭니다.

코드 위의 위치 안내를 먼저 확인합니다

작업 파일 · 넣을 위치 · 이번 작업은 코드 위에 항상 표시됩니다. 추가·수정할 코드는 본문에 바로 보이며, 적용 후 전체 코드만 눌러 펼칩니다. 필요한 변경만 작성하거나, 전체 파일을 펼쳐 앞뒤 위치와 중괄호를 함께 확인합니다.

코드 안의 노란색 줄은 이번 단계의 핵심 추가·수정 부분입니다. 주석의 // 이번 추가도 앞 단계에서 바뀐 부분을 알려 줍니다. 기존 필드·메서드도 함께 보면서 class 안에서 코드가 놓이는 위치와 실행 순서를 따라갑니다.

중괄호를 기준으로 넣을 위치를 구분합니다
안내실제 위치
class 안 · 메서드 밖public class PlayerProgress : MonoBehaviour 다음의 { } 안입니다. 필드나 새 메서드를 추가할 때 사용합니다.
메서드 안OnTriggerEnter2D(...)처럼 해당 메서드 이름 다음의 { } 안입니다. 접촉할 때 실행할 조건과 호출을 넣는 곳입니다.
파일 전체첫 줄 using UnityEngine;부터 맨 마지막 }까지 모두 포함합니다.

새 파일은 Assets/Scripts에 만듭니다. 기존 파일이 다른 폴더에 있으면 그 파일을 그대로 열어 수정합니다. 아래 파일명은 Project 창에서 검색할 이름입니다.

01 · PlayerProgress 생성과 수집 개수 저장

보석은 얻을 때마다 비활성화되지만 Player는 계속 움직입니다. 전체 수집 개수를 Player에 붙은 PlayerProgress 하나에 모으면, 어떤 보석을 먼저 얻어도 같은 기록을 이어 갈 수 있습니다.

직접 만든 컴포넌트PlayerProgress

이번 수업에서 만드는 컴포넌트로, Player의 수집 개수와 시작 위치를 저장합니다.

세 Collectible이 각각 독립된 연결로 Player 옆의 PlayerProgress 하나에 수집을 알리는 입체 모형
개념 그림 · 각 보석은 같은 PlayerProgress에 수집을 알립니다. 배선과 표시등은 호출과 기록을 설명하는 비유이며, 실제 실습에서는 Console로 개수를 확인합니다.

지금은 두 정수만 구분합니다. requiredCollectibles는 미리 정하는 필요 개수이고, collectedCount는 실행하면서 늘어나는 현재 개수입니다.

첫 수집 전후
값시작할 때첫 수집 후
requiredCollectibles3 · Inspector 설정3 · 그대로
collectedCount0 · int의 기본값1 · 수집할 때 증가
새 문법++

정수 변수 뒤의 ++는 그 변수에 저장된 값을 1 늘립니다.

직접 만든 메서드AddCollectible()

PlayerProgress에 직접 만든 메서드로, 수집 개수를 1 늘리고 현재 개수를 Console에 출력합니다.

PlayerProgress.cs를 만들고 첫 코드를 작성합니다

Play를 종료하고 Assets/Scripts에 PlayerProgress 이름의 MonoBehaviour Script를 생성합니다. 파일을 열고 아래 추가 위치에 맞춰 작성합니다. 기본 템플릿을 통째로 바꾸려면 ‘적용 후 전체 코드’를 사용합니다. 지금은 수집 개수와 AddCollectible만 작성하고, 시작 위치와 나머지 메서드는 뒤에서 추가합니다.

설정 한 줄[RequireComponent]

스크립트를 붙일 때 지정한 컴포넌트가 없다면 함께 추가하도록 선언하는 속성으로, 여기서는 Rigidbody2D를 요구합니다.

작성 안내 · 수집 개수 저장
작업 파일
PlayerProgress.cs
넣을 위치
새 class 안에 수집 필드와 AddCollectible을 넣고, class 선언 바로 위에 RequireComponent를 추가합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · PlayerProgress.cs · 수집 개수 저장

01 · 추가 위치 — PlayerProgress.cs에서 using UnityEngine; 다음, public class PlayerProgress : MonoBehaviour 바로 위에 추가합니다.

PlayerProgress.cs · 추가할 코드
[RequireComponent(typeof(Rigidbody2D))]

02 · 추가 위치 — PlayerProgress class 안의 기본 Start()·Update()를 지우고 아래 필드와 메서드를 넣습니다. class 선언과 바깥 중괄호는 유지합니다.

PlayerProgress.cs · 추가할 코드
// Player가 계속 보관할 수집 상태
[SerializeField, Min(1)]
private int requiredCollectibles = 3;

private int collectedCount;

// 보석이 호출하면 이 Player의 개수를 늘립니다.
public void AddCollectible()
{
    collectedCount++;
    Debug.Log("수집: " + collectedCount + " / " + requiredCollectibles);
}
적용 후 전체 코드 · PlayerProgress.cs · 수집 개수 저장

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

PlayerProgress.cs · 첫 단계 전체 코드
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerProgress : MonoBehaviour
{
    // Player가 계속 보관할 수집 상태
    [SerializeField, Min(1)]
    private int requiredCollectibles = 3;

    private int collectedCount;

    // 보석이 호출하면 이 Player의 개수를 늘립니다.
    public void AddCollectible()
    {
        collectedCount++;
        Debug.Log("수집: " + collectedCount + " / " + requiredCollectibles);
    }
}

public은 다른 스크립트에서 이 메서드를 호출할 수 있게 합니다. void는 실행을 마칠 때 결과값을 돌려주지 않는다는 뜻입니다. Required Collectibles를 3으로 정해도 현재 개수는 0에서 시작합니다. 이제 이 메서드를 실행할 대상을 찾아봅시다.

참고 · 필요 개수를 Inspector에 표시하는 설정
설정 한 줄[SerializeField] · [Min(1)]

[SerializeField]는 private 필드를 Inspector에서 설정할 수 있게 하고, [Min(1)]은 Inspector에서 입력할 최솟값을 1로 제한합니다.

위 전체 파일의 class 첫 부분에서 requiredCollectibles와 collectedCount를 함께 확인합니다. 두 필드는 AddCollectible의 중괄호 바깥에 있습니다.

SerializeField와 Min은 Inspector에서 필요 개수를 설정할 때 사용합니다. Min은 Inspector 입력을 돕고, 실행 중 수집 처리와 조건 검사는 메서드가 담당합니다.

  1. 저장하고 Player에 연결

    파일을 저장하고 Unity에서 컴파일이 끝나면 Player에 PlayerProgress를 하나 붙입니다. Required Collectibles는 3으로 둡니다. Player Tag, PlayerProgress, Player의 Collider2D는 같은 GameObject에 둡니다. 기존 PlayerController는 유지합니다.

    확인Console 오류 0개 · PlayerProgress 하나 · Required Collectibles 3
첫 번째 스크립트 준비 완료

개수를 저장하고 늘릴 메서드가 생겼습니다. 다음 단계에서 보석이 이 메서드를 호출하도록 연결합니다.

다음 질문

개수를 바꾸는 메서드는 PlayerProgress에 있습니다. 보석에 붙은 Collectible은 어떻게 이 메서드를 실행할까요?

02 · Collectible 수정과 메서드 호출

기존 Collectible.cs를 열고 네 단계로 연결 흐름을 읽습니다. Inspector에서 두 스크립트의 위치를 확인하고, 필요한 컴포넌트를 찾은 뒤 메서드를 호출합니다. 먼저 찾기에 성공한 흐름을 따라가고, 마지막에 찾지 못했을 때의 처리를 연결합니다.

1교시 Collectible.cs에 PlayerProgress를 찾고 AddCollectible을 호출하는 처리를 더합니다. 아래 추가·수정할 코드를 작성하고, 그 안에서 찾기 → 실패 검사 → 호출 → 비활성화 순서를 읽습니다. 다음 중복 수집 방지에서는 이 파일에 처리 여부를 더합니다.

01 · GameObject와 컴포넌트 구조

Play를 종료하고 Hierarchy에서 보석을 선택합니다. Inspector의 Collectible 컴포넌트를 가리킨 뒤 Player를 선택해 PlayerProgress와 Collider2D를 확인합니다. Project의 .cs 파일에서 정의한 기능이 Scene의 어느 GameObject에 컴포넌트로 붙어 있는지 연결해서 봅니다.

Inspector에서 번갈아 확인할 두 오브젝트
선택할 GameObject확인할 컴포넌트이번 수집에서 하는 일
보석 · CollectibleCollider2D(Is Trigger 켬) · CollectibleCollectible의 OnTriggerEnter2D가 접촉을 받습니다.
PlayerCollider2D · PlayerProgressPlayerProgress의 AddCollectible이 개수를 늘립니다.

두 스크립트는 서로 다른 GameObject에 붙어 있습니다. 실행 중인 Collectible에서, Player에 붙은 PlayerProgress의 기능을 사용하려는 상황입니다. Player의 기존 Rigidbody2D와 PlayerController는 이동을 계속 담당합니다.

화면에서 가리키기

접촉을 받는 컴포넌트와 개수를 저장하는 컴포넌트를 각각 가리켜 봅시다. 이제 보석의 코드가 PlayerProgress를 찾도록 연결합니다.

Collectible.cs 전체 파일을 함께 작성합니다

작성 안내 · 수집 연결
작업 파일
Collectible.cs
넣을 위치
기존 OnTriggerEnter2D 메서드 전체를 교체합니다. PlayerProgress 찾기와 AddCollectible 호출을 더합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · Collectible.cs · 수집 연결

01 · 교체 위치 — Collectible.cs의 기존 OnTriggerEnter2D 선언부터 그 메서드의 닫는 중괄호까지 아래 코드로 교체합니다. Player Tag 검사 뒤에 검색·실패 처리·수집 호출이 추가됩니다.

Collectible.cs · 교체할 코드
private void OnTriggerEnter2D(Collider2D other)
{
    Debug.Log("[접촉] " + other.name);

    // 1교시에서 작성한 Player Tag 검사
    if (!other.CompareTag("Player"))
    {
        Debug.Log("[중단] Player Tag가 아닙니다.");
        return;
    }

    // 이번 추가 1: 접촉한 Player의 진행도 컴포넌트를 찾습니다.
    bool found = other.TryGetComponent(out PlayerProgress progress);
    if (!found)
    {
        Debug.Log("[중단] PlayerProgress가 없습니다.");
        return;
    }

    // 이번 추가 2: PlayerProgress.cs의 메서드를 호출합니다.
    progress.AddCollectible();

    // 호출을 마치면 이곳으로 돌아와 보석을 끕니다.
    Debug.Log("[수집] Collectible 비활성화");
    gameObject.SetActive(false);
}
적용 후 전체 코드 · Collectible.cs · 수집 연결

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

Collectible.cs · 수집을 연결한 전체 코드
using UnityEngine;

public class Collectible : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D other)
    {
        Debug.Log("[접촉] " + other.name);

        // 1교시에서 작성한 Player Tag 검사
        if (!other.CompareTag("Player"))
        {
            Debug.Log("[중단] Player Tag가 아닙니다.");
            return;
        }

        // 이번 추가 1: 접촉한 Player의 진행도 컴포넌트를 찾습니다.
        bool found = other.TryGetComponent(out PlayerProgress progress);
        if (!found)
        {
            Debug.Log("[중단] PlayerProgress가 없습니다.");
            return;
        }

        // 이번 추가 2: PlayerProgress.cs의 메서드를 호출합니다.
        progress.AddCollectible();

        // 호출을 마치면 이곳으로 돌아와 보석을 끕니다.
        Debug.Log("[수집] Collectible 비활성화");
        gameObject.SetActive(false);
    }
}

저장하고 Unity로 돌아와 컴파일 오류가 0개인지 확인합니다. 아래 설명은 방금 작성한 전체 코드 안의 줄을 순서대로 읽습니다.

02 · 컴포넌트 검색과 참조

1교시의 other는 이번에 들어온 상대의 Collider였습니다. other.를 앞에 붙이면 그 Collider가 속한 GameObject에서 컴포넌트를 찾습니다. Player의 Collider와 PlayerProgress가 같은 GameObject에 있어야 하는 이유입니다.

새 기능TryGetComponent

호출 대상과 같은 GameObject에서 지정한 컴포넌트를 찾고, 있으면 true, 없으면 false를 반환합니다.

새 문법out

메서드가 처리한 결과를 호출한 쪽의 변수로 전달하는 문법으로, 여기서는 찾은 PlayerProgress 참조를 progress에 받습니다.

문법 복습bool · true · false

bool은 참과 거짓을 저장하는 자료형이며, 여기서는 찾기 성공 여부를 true 또는 false로 받습니다.

찾기와 조건 판단을 나누어 읽기 위해, 성공 여부를 found라는 변수에 받아 봅니다. 위 전체 코드의 bool found = other.TryGetComponent(out PlayerProgress progress); 줄을 확인합니다. OnTriggerEnter2D의 중괄호 안, Player Tag 검사 다음에 있습니다.

코드에 등장하는 이름의 역할을 구분합니다
표현의미
other이번에 접촉한 상대의 Collider입니다. 여기서는 Player의 Collider입니다.
PlayerProgress찾으려는 컴포넌트의 종류, 즉 자료형 이름입니다.
progress찾은 PlayerProgress 컴포넌트를 가리킬 변수 이름입니다.
out찾은 참조를 progress에 전달하도록 표시합니다.
found찾기에 성공했는지 저장하는 bool 변수입니다.

PlayerProgress는 찾을 종류이고, progress는 찾은 대상을 가리키는 이름입니다. 예를 들어 int count에서 int가 자료형이고 count가 변수 이름이듯, PlayerProgress progress도 같은 순서로 읽습니다. 여기서 progress가 가리키는 대상은 Inspector에서 확인한 기존 PlayerProgress 컴포넌트입니다.

한 번의 찾기에서 두 결과를 받습니다
결과를 받는 곳PlayerProgress를 찾았을 때
foundtrue · 찾기에 성공했습니다.
progress접촉한 Player에 붙은 PlayerProgress의 참조입니다.

다음 단계는 찾기에 성공한 경우입니다. 이 참조를 통해 PlayerProgress에 있는 메서드를 실행합니다.

03 · 메서드 호출과 실행 복귀

위 Collectible.cs 전체 코드에서 // 이번 추가 2 바로 다음 줄을 가리킵니다. progress.AddCollectible();은 “progress가 가리키는 PlayerProgress의 AddCollectible을 지금 실행한다”는 뜻입니다.

호출한 줄에서 PlayerProgress.cs의 AddCollectible 메서드로 이동해 본문을 읽습니다. 앞에서 작성한 PlayerProgress 전체 코드에서 메서드 안의 두 줄을 확인합니다. 처음 개수가 0이라면 1로 바뀌고, 수집: 1 / 3이 출력됩니다.

메서드의 끝에 도달하면 다시 Collectible.cs의 호출한 줄 바로 다음 줄로 돌아옵니다. 이어서 비활성화 로그를 출력하고 보석을 끕니다. AddCollectible의 void는 결과값을 돌려주지 않는다는 뜻이며, 호출한 쪽의 다음 코드로 이어지는 실행 흐름은 그대로입니다.

  1. 01 · Collectible.cs호출

    progress.AddCollectible();에서 PlayerProgress의 메서드를 실행합니다.

  2. 02 · PlayerProgress.cs기록

    개수를 1 늘리고 수집: 1 / 3을 출력합니다.

  3. 03 · Collectible.cs복귀

    호출 다음 줄부터 이어서 실행하고 보석을 비활성화합니다.

한 번의 접촉 안에서 호출 → 개수 변경 → 호출 다음 줄로 복귀 → 보석 비활성화가 순서대로 이어집니다.
대상과 순서 설명하기

왜 progress.AddCollectible()은 Player의 개수를 바꾸고, gameObject.SetActive(false)는 보석을 끌까요? 두 줄 사이에 어느 파일의 코드가 실행되는지도 설명해 봅시다.

해설 · 점 앞의 대상과 현재 실행 위치를 확인합니다

progress는 Player의 PlayerProgress를 가리킵니다. 그 참조로 AddCollectible을 호출하면 해당 컴포넌트의 collectedCount가 늘어납니다. 호출을 마치고 돌아온 Collectible.cs에서 gameObject는 이 스크립트가 붙은 보석입니다. 따라서 개수는 Player에 남고 보석이 비활성화됩니다.

04 · 컴포넌트 검색 실패 처리

지금까지는 PlayerProgress를 찾은 경우를 읽었습니다. 이번에는 Player Tag는 맞지만 필요한 컴포넌트가 없는 경우를 예상합니다. 찾기에 실패하면 found는 false이고, progress에는 사용할 컴포넌트 참조가 없습니다. 이때는 AddCollectible을 호출하기 전에 실행을 끝내야 합니다.

1교시의 !와 return을 이어 씁니다
찾기 결과!found이후 흐름
성공 · found = truefalseif를 지나 수집 처리를 계속합니다.
실패 · found = falsetrue중단 로그를 출력하고 이번 실행을 끝냅니다.

위 전체 코드에서는 bool found = ...; 다음의 if (!found)가 실패를 검사합니다. 다음 단계의 전체 코드에서는 이를 if (!other.TryGetComponent(out PlayerProgress progress))로 묶습니다. 두 형태 모두 찾지 못했을 때 호출 전에 멈추는 순서입니다.

이 검사를 통과하면 progress로 메서드를 호출할 수 있습니다. 다음 단계에서는 Collectible.cs 전체를 새 모습으로 갱신하므로, found를 사용하는 검사와 한 줄로 묶은 검사가 중복되지 않습니다.

복습 · GetComponent와 other.TryGetComponent의 찾는 위치
점 앞의 대상을 확인합니다
표현찾는 위치
GetComponent<Rigidbody2D>()현재 스크립트가 붙은 GameObject
other.TryGetComponent(out PlayerProgress progress)other Collider가 붙은 GameObject

Collectible에서 대상 없이 GetComponent<PlayerProgress>()를 호출하면 보석 자신에서 찾습니다. 이번 TryGetComponent는 부모·자식까지 찾아다니지 않으므로 Player의 Collider와 PlayerProgress를 같은 GameObject에 둡니다.

로그로 설명하기

[접촉] Player 뒤에 [중단] PlayerProgress가 없습니다.가 나왔습니다. 어디까지 실행되었고, Inspector의 어느 오브젝트를 확인해야 할까요?

해설 · Player에서 컴포넌트의 연결 위치를 확인합니다

접촉 메서드가 실행되었고 앞의 Player Tag 검사도 통과했습니다. PlayerProgress를 찾는 단계에서 중단했으므로 개수와 보석의 활성 상태는 그대로입니다. 접촉한 Player Collider와 같은 GameObject에 PlayerProgress가 있는지 확인한 뒤 새 Play에서 다시 시험합니다.

이제 필요한 기능을 찾아 실행하는 과정이 연결되었습니다. 마지막으로, 이미 얻은 보석이 다시 수집을 요청하지 않도록 각 보석의 처리 여부를 확인합니다.

중복 수집 방지

마지막으로 중복 처리 여부를 확인합니다. 전체 개수는 Player가 기억하고, “이 보석을 이미 얻었는가?”는 각 Collectible의 collected가 따로 기억합니다. 별도 값을 지정하지 않은 bool 필드는 false로 시작합니다. 보석 A를 얻어도 아직 남아 있는 보석 B의 collected는 false입니다.

새 문법||

두 조건 중 하나라도 true이면 전체가 true인 OR 연산자로, 왼쪽이 true이면 오른쪽 조건은 평가하지 않습니다.

아래 전체 코드에서 private bool collected;는 class 안, OnTriggerEnter2D 밖에 있습니다. if (collected || !other.CompareTag("Player"))는 OnTriggerEnter2D 안, 접촉 로그 다음에 있습니다.

수집 처리를 진행할 경우는 하나입니다
이미 수집했나?상대가 Player인가?결과
아니오예PlayerProgress를 찾아 수집 진행
아니오아니오중단
예예 또는 아니오중단

전체 코드에서는 이 검사가 PlayerProgress 찾기보다 먼저 실행됩니다. 이미 얻었거나 Player가 아닌 경우를 먼저 끝내고, 나머지 경우에 필요한 컴포넌트를 찾습니다. 찾기에 성공하면 collected = true;를 기록한 뒤 개수를 늘리고 보석을 끕니다.

전체 코드로 돌아와 순서를 확인합니다
순서역할
1 · 대상과 처리 여부 검사이미 수집했거나 Player Tag가 아니면 중단합니다.
2 · PlayerProgress 찾기필요한 컴포넌트가 없으면 중단합니다.
3 · collected = true이 보석을 처리했다고 기록합니다.
4 · AddCollectible 호출과 복귀Player의 개수를 늘린 뒤 Collectible의 다음 줄로 돌아옵니다.
5 · 보석 비활성화로그를 남기고 gameObject.SetActive(false)를 실행합니다.

처리 여부를 기억하는 것과 화면에서 사라지게 하는 것은 서로 다른 역할입니다. 아래 Collectible 전체 코드에서 다섯 순서를 확인하고 기존 파일에 반영합니다.

Collectible.cs를 완성하고 저장합니다

1교시부터 이어 쓴 Collectible.cs를 엽니다. ‘추가·수정할 코드’에서 필드 추가와 메서드 교체를 진행하고, ‘적용 후 전체 코드’로 파일 구성을 비교합니다. 같은 class나 OnTriggerEnter2D를 두 번 선언하지 않습니다. 파일이 없다면 Assets/Scripts에 Collectible 이름으로 생성한 뒤 작성합니다.

작성 안내 · 중복 방지
작업 파일
Collectible.cs
넣을 위치
class 안·메서드 밖에 collected 필드를 추가하고, 기존 OnTriggerEnter2D 메서드를 교체합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · Collectible.cs · 중복 방지

01 · 추가 위치 — Collectible class의 여는 중괄호 바로 아래, OnTriggerEnter2D 바깥에 추가합니다.

Collectible.cs · 추가할 코드
private bool collected;

02 · 교체 위치 — 같은 파일의 기존 OnTriggerEnter2D 메서드 전체를 교체합니다. 첫 if의 중복 검사, 검색과 실패 검사를 묶은 if, 호출 전 collected = true;가 바뀐 부분입니다.

Collectible.cs · 교체할 코드
private void OnTriggerEnter2D(Collider2D other)
{
    Debug.Log("[접촉] " + other.name);

    if (collected || !other.CompareTag("Player"))
    {
        Debug.Log("[중단] 이미 수집했거나 Player Tag가 아닙니다.");
        return;
    }

    if (!other.TryGetComponent(out PlayerProgress progress))
    {
        Debug.Log("[중단] PlayerProgress가 없습니다.");
        return;
    }

    collected = true;
    progress.AddCollectible();
    Debug.Log("[수집] Collectible 비활성화");
    gameObject.SetActive(false);
}
적용 후 전체 코드 · Collectible.cs · 중복 방지

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

Collectible.cs · 직접 작성할 전체 코드
using UnityEngine;

public class Collectible : MonoBehaviour
{
    private bool collected;

    private void OnTriggerEnter2D(Collider2D other)
    {
        Debug.Log("[접촉] " + other.name);

        if (collected || !other.CompareTag("Player"))
        {
            Debug.Log("[중단] 이미 수집했거나 Player Tag가 아닙니다.");
            return;
        }

        if (!other.TryGetComponent(out PlayerProgress progress))
        {
            Debug.Log("[중단] PlayerProgress가 없습니다.");
            return;
        }

        collected = true;
        progress.AddCollectible();
        Debug.Log("[수집] Collectible 비활성화");
        gameObject.SetActive(false);
    }
}
두 번째 스크립트 준비 완료

저장 후 Unity에서 오류가 0개인지 확인합니다. 보석에는 기존 Sprite Renderer, Is Trigger를 켠 Collider2D, Collectible 컴포넌트 하나를 둡니다. Player의 PlayerProgress 연결도 확인하고 아래 첫 수집을 실행합니다.

수집 규칙은 이제 연결되었습니다. 먼저 보석 하나로 실행을 확인한 뒤, 위험에 닿았을 때 무엇을 되돌리고 무엇을 남길지 정합니다.

수집 기능 실행 확인

1교시와 같은 보석을 향해 움직입니다. 겉으로는 보석이 사라지는 모습이 같지만, 이번에는 Player에 수집 개수도 남습니다. 그림의 변화와 Console의 기록을 함께 확인하세요.

  1. 수집 기능 실행

    Console을 Clear하고 Play합니다. 기존 이동·점프를 사용해 보석에 닿습니다.

    확인보석이 비활성화되고 수집: 1 / 3이 출력됩니다.
  2. 비활성화 및 재접촉 확인

    보석이 있던 자리를 한 번 더 통과합니다. 관찰을 마치면 Play를 종료합니다.

    확인개수는 추가로 늘지 않습니다. Play를 끝내면 보석이 원래 상태로 돌아옵니다.
Player가 첫 보석에 닿았을 때 · Player라는 이름을 사용한 예
[접촉] Player
수집: 1 / 3
[수집] Collectible 비활성화

첫 줄의 이름은 실제로 접촉한 오브젝트 이름에 따라 달라집니다. 수집: 1 / 3에서 1은 지금 모은 개수이고, 3은 목표에 필요한 개수입니다. 앞에서 만든 PlayerProgress가 개수를 저장하고 Collectible이 AddCollectible을 호출한 결과입니다.

보석이 그대로라면 · 멈춘 단계를 로그로 찾습니다
1교시의 접촉 확인을 이어 씁니다
관찰먼저 확인할 곳
[접촉] 로그도 없음Console 로그 표시·필터를 확인한 뒤 Trigger, Rigidbody2D, Collider2D, Collectible 연결과 실제 겹침을 확인합니다.
[중단] 이미 수집했거나 Player Tag가 아닙니다.첫 수집 전이라면 상대의 Player Tag를 확인합니다. 같은 Play에서 수집물을 다시 켰다면 중복 방지가 작동한 것입니다.
[중단] PlayerProgress가 없습니다.Player의 Collider2D와 같은 GameObject에 PlayerProgress를 붙였는지 확인합니다.
수집 로그는 있지만 기대한 개수와 다름새 Play인지, Collectible·PlayerProgress가 중복 연결되지 않았는지 확인합니다.

로그는 실행이 어디까지 왔는지 보여 주는 관찰 도구입니다. 설정을 고친 뒤 Play 종료 → Console Clear → 새 Play 순서로 같은 접촉을 다시 확인합니다.

선택 실험 · 같은 Play에서 보석을 다시 켜면?

새 Play에서 보석 하나를 수집한 뒤 Play를 유지한 채 Hierarchy에서 그 보석을 다시 활성화합니다. Player가 영역 밖으로 나왔다가 다시 들어오게 합니다. 그림은 돌아와도 collected가 true로 남아 있어 개수가 늘지 않고 중단 로그가 출력됩니다. Play를 종료한 뒤 새로 실행하면 초기 상태에서 다시 수집할 수 있습니다. 실험 후 Play를 종료합니다.

시작 위치 저장

위험에 닿았을 때 시작점으로 돌아오려면 출발한 위치를 남겨 두어야 합니다. PlayerProgress는 개수와 함께 이 좌표도 보관합니다. 4주차에서 사용한 Rigidbody2D의 참조를 얻고, 이동하기 전 위치를 한 번 읽습니다.

새 기능Rigidbody2D.position

Rigidbody2D의 현재 2D 위치를 읽거나 지정하는 속성입니다.

실행 순서 복습Awake · Start

Awake에서 컴포넌트 참조를 준비하고, 이 장면에서는 첫 프레임의 이동 처리가 시작되기 전 Start에서 출발 좌표를 저장합니다.

Play를 종료하고 PlayerProgress.cs를 다시 엽니다. 아래는 수집 코드에 시작 위치 저장까지 더한 전체 파일입니다. 기존 AddCollectible 위에 body·startPosition 필드와 Awake·Start가 들어가는 모습을 함께 작성합니다.

작성 안내 · 시작 위치 저장
작업 파일
PlayerProgress.cs
넣을 위치
collectedCount 아래, AddCollectible 위에 body·startPosition 필드와 Awake·Start 메서드를 추가합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · PlayerProgress.cs · 시작 위치 저장

01 · 추가 위치 — PlayerProgress.cs에서 private int collectedCount; 아래, 기존 AddCollectible() 바로 위에 넣습니다. 두 필드와 Awake·Start 모두 class 안이며 기존 메서드 바깥입니다.

PlayerProgress.cs · 추가할 코드
private Rigidbody2D body;
private Vector2 startPosition;

private void Awake()
{
    body = GetComponent<Rigidbody2D>();
}

private void Start()
{
    startPosition = body.position;
}
적용 후 전체 코드 · PlayerProgress.cs · 시작 위치 저장

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

PlayerProgress.cs · 시작 위치 저장까지 전체 코드
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerProgress : MonoBehaviour
{
    [SerializeField, Min(1)]
    private int requiredCollectibles = 3;

    private int collectedCount;

    // 이번 추가 1: class 안에서 참조와 시작 좌표를 보관합니다.
    private Rigidbody2D body;
    private Vector2 startPosition;

    // 이번 추가 2: 참조를 준비하고 첫 위치를 한 번 저장합니다.
    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    private void Start()
    {
        startPosition = body.position;
    }

    // 기존 수집 메서드는 같은 class 안에 그대로 둡니다.
    public void AddCollectible()
    {
        collectedCount++;
        Debug.Log("수집: " + collectedCount + " / " + requiredCollectibles);
    }
}
시작 위치가 (-6, 1)인 예
시점body.positionstartPosition
출발 직후(-6, 1)(-6, 1)
오른쪽으로 이동한 뒤(2, 1)(-6, 1)

body는 움직이는 Rigidbody2D를 가리키고, startPosition은 출발할 때 읽은 좌표 값을 보관합니다. 몸체가 움직여도 복사해 둔 좌표가 함께 바뀌지는 않습니다. Update에서 매번 저장하면 복귀 기준도 Player를 따라 움직이므로, 이번 규칙에서는 Start에서 한 번만 기록합니다.

실행 전에 예상하기

Player가 (2, 1)까지 이동한 뒤 위험에 닿으면 어느 좌표로 돌아와야 할까요? 이제 위험 구역을 연결해 확인합니다.

03 · HazardZone 생성과 복귀 처리

이번 위험 규칙은 수집 기록을 유지한 채 출발점으로 돌아오기입니다. 돌아온 위치는 같아도 이미 얻은 보석까지 되돌릴 필요는 없습니다. 무엇을 바꾸는지 코드에서 확인합니다.

직접 만든 메서드ResetToStart()

PlayerProgress에 직접 만든 메서드로, Player의 속도를 0으로 바꾸고 저장한 시작 위치로 옮깁니다.

새 기능Vector2.zero

x와 y가 모두 0인 Vector2 값이며, 여기서는 복귀 순간의 속도를 (0, 0)으로 지정할 때 사용합니다.

먼저 PlayerProgress.cs를 아래 전체 코드로 갱신합니다. 기존 수집·시작 위치 코드 아래에 ResetToStart가 추가됩니다. AddCollectible과 같은 깊이의 메서드인지 확인한 뒤 저장하고, 다음 HazardZone.cs를 작성합니다.

작성 안내 · 위험 복귀
작업 파일
PlayerProgress.cs
넣을 위치
AddCollectible 다음, class의 마지막 중괄호 앞에 ResetToStart를 추가합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · PlayerProgress.cs · 위험 복귀

01 · 추가 위치 — PlayerProgress.cs의 AddCollectible()를 닫는 중괄호 다음, class의 마지막 중괄호 앞에 넣습니다. 기존 필드와 메서드는 유지합니다.

PlayerProgress.cs · 추가할 코드
public void ResetToStart()
{
    body.linearVelocity = Vector2.zero;
    body.position = startPosition;
    Debug.Log("시작점 복귀 / 수집: " + collectedCount);
}
적용 후 전체 코드 · PlayerProgress.cs · 위험 복귀

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

PlayerProgress.cs · 위험 복귀까지 전체 코드
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerProgress : MonoBehaviour
{
    [SerializeField, Min(1)]
    private int requiredCollectibles = 3;

    private int collectedCount;

    private Rigidbody2D body;
    private Vector2 startPosition;

    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    private void Start()
    {
        startPosition = body.position;
    }

    public void AddCollectible()
    {
        collectedCount++;
        Debug.Log("수집: " + collectedCount + " / " + requiredCollectibles);
    }

    // 이번 추가: 수집 기록은 유지하고 속도와 위치만 되돌립니다.
    public void ResetToStart()
    {
        body.linearVelocity = Vector2.zero;
        body.position = startPosition;
        Debug.Log("시작점 복귀 / 수집: " + collectedCount);
    }
}

linearVelocity는 4주차에서 다룬 Rigidbody2D의 속도입니다. 위치만 옮기면 떨어지던 속도가 남을 수 있으므로 함께 초기화합니다. 계속 정지시키는 코드는 아닙니다. 이후 입력과 중력이 다시 작용하므로 이동 키를 누른 채 돌아오면 곧바로 움직일 수 있습니다.

HazardZone.cs를 생성하고 복귀 요청을 작성합니다

Assets/Scripts에 HazardZone 이름으로 MonoBehaviour Script를 생성하고 추가·수정할 코드의 안내에 맞춰 기본 템플릿에 작성합니다. Player인지 확인한 뒤, 방금 만든 ResetToStart를 호출합니다. 저장하고 Unity에서 컴파일 오류가 0개인지 확인합니다.

작성 안내 · 복귀 요청
작업 파일
HazardZone.cs
넣을 위치
새 class 안의 기본 Start·Update를 지우고 OnTriggerEnter2D를 작성합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · HazardZone.cs · 복귀 요청

01 · 추가 위치 — 새 HazardZone.cs의 class 안에서 기본 Start()·Update()를 지우고 아래 메서드를 넣습니다. using UnityEngine;, class 선언, 바깥 중괄호는 유지합니다.

HazardZone.cs · 추가할 코드
private void OnTriggerEnter2D(Collider2D other)
{
    if (!other.CompareTag("Player"))
    {
        return;
    }

    if (other.TryGetComponent(out PlayerProgress progress))
    {
        progress.ResetToStart();
    }
}
적용 후 전체 코드 · HazardZone.cs · 복귀 요청

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

HazardZone.cs · 직접 작성할 전체 코드
using UnityEngine;

public class HazardZone : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (!other.CompareTag("Player"))
        {
            return;
        }

        if (other.TryGetComponent(out PlayerProgress progress))
        {
            progress.ResetToStart();
        }
    }
}
짙은 돌 받침 위에 붉은 가시 다섯 개가 놓인 투명 배경 픽셀 아트 스프라이트
HazardSpikes · 기존 캐릭터와 지형에 맞춘 위험 구역 스프라이트입니다. 가시 이미지 전체를 하나의 Trigger 구역으로 사용합니다.

위험 구역 스프라이트 PNG 내려받기 ↓

스프라이트 가져오기 및 크기 설정

파일이 없다면 PNG를 내려받아 Assets/Week5/Sprites에 넣습니다. 이미 준비된 파일이 있으면 그대로 사용합니다. Project에서 HazardSpikes.png를 선택하고 다음 설정을 확인한 뒤 Apply합니다.

HazardSpikes 가져오기 설정
항목설정
Texture Type / Sprite ModeSprite (2D and UI) / Single
Pixels Per Unit / Pivot1024 / Center
Filter Mode / CompressionPoint (no filter) / None
Max Size / Alpha Is Transparency4096 / 켬

Transform Scale이 (1, 1, 1)일 때 보이는 가시의 폭은 약 2 unit입니다. Box Collider 2D는 Size (2.01, 0.38), Offset (0, -0.01)을 시작값으로 두고 Edit Collider로 그림과 범위를 확인합니다. 투명 여백까지 넓게 잡히지 않도록 조정합니다. PPU나 Pivot을 바꾸었다면 Collider도 다시 맞춥니다.

가시는 접촉을 알아보기 위한 그림입니다. 실제 복귀는 HazardZone 스크립트와 Trigger Collider가 처리합니다. 지형을 막는 일반 Collider를 추가하지 않습니다.

  1. 위험 구역 구성

    Play를 종료합니다. 기존 위험 구역이 있으면 Sprite Renderer의 Sprite를 HazardSpikes로 교체합니다. 없다면 Project의 HazardSpikes 스프라이트를 Scene으로 드래그하고 HazardZone으로 이름을 정합니다. Sprite Renderer의 Color는 흰색으로 둡니다. Box Collider 2D의 Is Trigger를 켜고 방금 작성한 HazardZone.cs를 하나 붙입니다. 이미 연결되어 있다면 기존 컴포넌트를 확인합니다. 가시가 바닥 위에 보이도록 배치하고 시작점과 보석에서는 떨어뜨립니다.

    확인Sprite Renderer · Trigger Collider · HazardZone이 같은 GameObject에 있습니다. 별도 Rigidbody2D는 추가하지 않습니다.

위 HazardZone.cs 전체 코드에서 progress.ResetToStart();는 if (other.TryGetComponent(...))의 중괄호 안에 있습니다. PlayerProgress를 찾았을 때만 호출합니다.

HazardZone은 접촉을 감지하고 PlayerProgress에 복귀를 요청합니다. 시작 좌표를 직접 저장하지 않으므로 위험 구역이 늘어나도 같은 Player의 출발점으로 돌아옵니다.

보석 한 개를 모은 뒤 위험에 닿으면
항목직후 결과
Player 위치저장한 시작 위치
Player 속도(0, 0)으로 지정
수집 개수1 유지
이미 얻은 보석비활성 상태 유지
연결 후 시연

Console을 비운 새 Play에서 보석 하나를 얻고 위험 구역에 닿습니다. 시작점 복귀 / 수집: 1과 실제 위치를 확인합니다. 보석은 다시 나타나지 않아야 합니다. 확인 후 Play를 종료합니다.

수치로 설명하기 · 위치 (2, 3), 속도 (5, -4), 수집 1개라면?

출발 위치가 (-6, 1)이었다면 ResetToStart 직후 위치는 (-6, 1), 속도는 (0, 0), 수집 개수는 1입니다. 이후 속도는 입력과 중력에 따라 다시 달라집니다. Scene 전체를 다시 불러온 결과와 구분합니다.

04 · GoalZone 생성과 완료 판정

보석은 값을 늘리고, 위험은 위치를 되돌렸습니다. 목표에서는 새로 수집하지 않고 지금까지 남은 기록을 읽습니다. GoalZone이 PlayerProgress에 “필요한 만큼 모았나요?”라고 묻도록 만듭니다.

직접 만든 메서드HasAllCollectibles()

PlayerProgress에 직접 만든 메서드로, 현재 수집 개수가 필요 개수 이상인지 true 또는 false로 알려 줍니다.

새 문법>=

왼쪽 값이 오른쪽 값보다 크거나 같으면 true가 되는 비교 연산자입니다.

새 문법return 값;

메서드 실행을 끝내면서 호출한 쪽으로 값을 돌려주는 문법으로, 이번 bool 메서드는 비교 결과를 반환합니다.

PlayerProgress.cs를 아래 최종 전체 코드로 갱신합니다. 이번에 달라지는 부분은 AddCollectible과 ResetToStart 사이의 HasAllCollectibles입니다. 수집 기록·시작 위치·복귀 코드도 같은 파일에 함께 둡니다.

작성 안내 · 목표 조건
작업 파일
PlayerProgress.cs
넣을 위치
AddCollectible 뒤, ResetToStart 앞에 HasAllCollectibles를 추가합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · PlayerProgress.cs · 목표 조건

01 · 추가 위치 — PlayerProgress.cs의 AddCollectible() 닫는 중괄호 뒤, ResetToStart() 선언 바로 앞에 넣습니다. 다른 메서드의 중괄호 바깥에 둡니다.

PlayerProgress.cs · 추가할 코드
public bool HasAllCollectibles()
{
    return collectedCount >= requiredCollectibles;
}
적용 후 전체 코드 · PlayerProgress.cs · 목표 조건

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

PlayerProgress.cs · 모든 단계를 마친 전체 코드
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerProgress : MonoBehaviour
{
    [SerializeField, Min(1)]
    private int requiredCollectibles = 3;

    private int collectedCount;
    private Rigidbody2D body;
    private Vector2 startPosition;

    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    private void Start()
    {
        startPosition = body.position;
    }

    public void AddCollectible()
    {
        collectedCount++;
        Debug.Log("수집: " + collectedCount + " / " + requiredCollectibles);
    }

    public bool HasAllCollectibles()
    {
        return collectedCount >= requiredCollectibles;
    }

    public void ResetToStart()
    {
        body.linearVelocity = Vector2.zero;
        body.position = startPosition;
        Debug.Log("시작점 복귀 / 수집: " + collectedCount);
    }
}

위 전체 파일을 저장한 뒤 세 public 메서드의 위치를 확인합니다. AddCollectible, HasAllCollectibles, ResetToStart는 class 안에서 나란히 있는 세 메서드이며, 서로의 중괄호 안에 들어가지 않습니다.

필요 개수가 3일 때
현재 개수비교반환값
22 >= 3false
33 >= 3true
44 >= 3true

return;은 이번 실행을 끝내고, return 비교식;은 실행을 끝내며 결과도 돌려줍니다. 이 질문을 여러 번 해도 개수는 늘지 않습니다. 정확히 같은 값만 보는 ==와 달리 >=는 필요 개수를 넘겨도 조건을 충족하게 합니다.

아래 GoalZone.cs 전체 파일의 OnTriggerEnter2D에서 PlayerProgress를 찾은 다음 if (!progress.HasAllCollectibles())를 확인합니다. 미완료이면 return하고, 통과하면 completed = true;와 성공 로그로 이어집니다.

2개라면 질문의 답은 false입니다. 앞의 !로 조건이 true가 되어 미완료 로그를 남기고 끝납니다. 3개라면 답이 true이므로 if를 지나 성공 처리로 내려갑니다. GoalZone의 completed는 성공을 이미 기록했는지 저장하는 bool 필드입니다. 첫 조건의 completed || !other.CompareTag("Player")로 재진입 때 같은 성공을 반복하지 않습니다.

GoalZone.cs를 생성하고 목표 조건을 작성합니다

Assets/Scripts에 GoalZone 이름으로 MonoBehaviour Script를 생성합니다. 추가·수정할 코드의 안내에 맞춰 기본 템플릿에 작성하고 저장합니다. HasAllCollectibles를 먼저 추가했으므로 이제 GoalZone에서 이 메서드를 호출할 수 있습니다.

작성 안내 · 완료 판정
작업 파일
GoalZone.cs
넣을 위치
새 class 안의 기본 Start·Update를 지우고 completed 필드와 OnTriggerEnter2D를 작성합니다.
이번 작업
아래 ‘추가·수정할 코드’를 보고 변경할 부분을 작성합니다. ‘적용 후 전체 코드’에서는 기존 코드를 포함한 파일 전체를 비교하거나 복사할 수 있습니다.

추가·수정할 코드 · GoalZone.cs · 완료 판정

01 · 추가 위치 — 새 GoalZone.cs의 class 안에서 기본 Start()·Update()를 지우고 아래 필드와 메서드를 넣습니다. using UnityEngine;, class 선언, 바깥 중괄호는 유지합니다.

GoalZone.cs · 추가할 코드
private bool completed;

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

    if (!other.TryGetComponent(out PlayerProgress progress))
    {
        return;
    }

    if (!progress.HasAllCollectibles())
    {
        Debug.Log("아직 수집물이 남았습니다.");
        return;
    }

    completed = true;
    Debug.Log("LEVEL COMPLETE");
}
적용 후 전체 코드 · GoalZone.cs · 완료 판정

이 단계까지 작성한 파일 전체입니다. 아래 코드를 복사할 때는 해당 파일의 첫 줄부터 마지막 중괄호까지 모두 교체합니다. 추가 코드와 중복해서 덧붙이지 않습니다.

GoalZone.cs · 직접 작성할 전체 코드
using UnityEngine;

public class GoalZone : MonoBehaviour
{
    private bool completed;

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

        if (!other.TryGetComponent(out PlayerProgress progress))
        {
            return;
        }

        if (!progress.HasAllCollectibles())
        {
            Debug.Log("아직 수집물이 남았습니다.");
            return;
        }

        completed = true;
        Debug.Log("LEVEL COMPLETE");
    }
}
네 번째 스크립트 작성 완료

Unity로 돌아와 컴파일 오류가 0개인지 확인합니다. 이제 목표 오브젝트에 연결하고 수집 전 진입부터 시험합니다.

  1. 목표 구역 구성

    Play를 종료합니다. 4주차 도착 표지판이 있다면 그 오브젝트를 사용합니다. 없다면 기존 GoalSign 스프라이트를 Scene에 놓습니다. GoalZone으로 이름을 정하고 Box Collider 2D의 Is Trigger를 켠 뒤 GoalZone.cs를 하나 붙입니다. 마지막 보석과 Trigger가 겹치지 않도록 떨어뜨립니다.

    확인Player가 수집 전에도 들어갔다 나올 수 있는 통과 구역입니다.

GoalSign 파일이 필요하면 4주차 GoalSign.png를 다시 사용할 수 있습니다. 캐릭터·보석·목표 표지판은 기존 그림을 재사용하고, 위험 구역에는 위에서 제공한 HazardSpikes.png를 사용합니다.

먼저 미완료 확인

Console을 비운 새 Play에서 보석을 얻기 전에 Goal에 들어갑니다. 아직 수집물이 남았습니다.를 확인하고 영역 밖으로 나옵니다. 확인 후 Play를 종료합니다. 세 개를 모은 성공 결과는 다음 단계에서 보석을 늘린 뒤 확인합니다.

목표는 통과 가능한 판정 구역입니다. 수집이 부족해도 문이 닫히지 않고, 성공해도 이동은 계속됩니다. 이번 수업의 성공 표시는 Console의 LEVEL COMPLETE입니다.

왜 마지막 보석과 Goal을 떨어뜨리나요?

현재 코드는 OnTriggerEnter2D에서 판정합니다. Goal 안에 머무르는 동안 개수가 늘어도 자동으로 다시 판정하지 않습니다. 두 영역이 겹치면 이벤트 처리 순서에 따라 관찰이 달라질 수 있으므로 분리합니다. 보석을 모은 뒤 Goal 밖에서 안으로 새로 들어가야 합니다.

Prefab 생성과 재사용

보석 하나의 수집을 확인했으므로 이제 세 개로 늘립니다. 1교시의 보석은 Assets/Week2/Prefabs/Collectible.prefab에서 가져온 instance입니다. 여기에 추가한 Trigger와 스크립트를 5주차용 구성으로 저장합니다.

새 기능Prefab Variant

기존 Prefab의 구성을 이어받으면서 변경한 설정과 컴포넌트를 별도로 저장하는 Prefab입니다.

  1. Prefab Variant 생성

    Play를 종료합니다. 검증한 Collectible을 Hierarchy에서 Assets/Prefabs로 드래그합니다. 선택 창에서 Prefab Variant를 선택하고 PF_Collectible로 이름을 정합니다. 앞 주차 원본에는 변경 사항을 Apply하지 않습니다.

    확인Project에 PF_Collectible이 생기고 원래 Collectible의 그림을 이어받습니다.
  2. Prefab 연결 확인

    Scene의 보석이 PF_Collectible을 가리키는지 확인합니다. 이전 원본을 가리킨다면 위치를 기록하고 PF_Collectible을 같은 위치에 배치한 뒤 기존 instance를 제거합니다. 새 원본을 열어 Is Trigger 켬과 Collectible 하나를 확인합니다.

    확인Scene에 PF_Collectible에서 온 보석 한 개가 있습니다.
  3. Prefab 인스턴스 배치

    PF_Collectible을 Scene에 두 번 더 드래그합니다. 총 세 개를 서로 떨어진, Player가 도달할 수 있는 곳에 놓습니다. 마지막 보석은 Goal 밖에 둡니다.

    확인세 보석의 Collider와 스크립트 구성은 같고 위치는 다릅니다.
같은 원본이어도 실행 중 상태는 각자입니다
저장할 상태담당예
이 보석을 얻었는가?각 Collectible의 collectedA를 얻어도 B와 C는 false
전체 몇 개를 모았는가?PlayerProgress의 collectedCountA → B → C 수집 시 1 → 2 → 3
성공을 이미 알렸는가?GoalZone의 completed첫 성공 뒤 true

Scene에 배치한 원본의 사용본을 instance라고 합니다. 원본과 다르게 바꾼 설정은 override입니다. 공통 Collider나 스크립트 구성은 PF_Collectible에서, 각 위치는 Scene에서 관리합니다. 하나만 다르게 반응하면 그 instance에 별도 변경이 있는지 확인합니다.

3교시로 이어 갈 범위

2교시 시연은 수집물 3개·위험 1개·목표 1개로 확인합니다. 3교시에는 위험·목표도 Prefab으로 저장하고 위험을 두 개로 늘려 자신의 경로를 구성합니다.

상호작용 통합 검증

각 규칙을 따로 확인했다면 한 장면에서 이어 봅니다. 아래 기본 흐름을 한 번 시연합니다. Console을 비우고 새 Play를 시작하며, 다음 행동 전에 예상 개수를 먼저 말해 봅니다.

필요 개수 3 · 새 Play에서 시작
순서행동남는 기록과 결과
1보석을 얻기 전에 Goal 진입 후 빠져나오기개수 0 · 아직 수집물이 남았습니다.
2보석 A 수집수집: 1 / 3 · A 비활성화
3위험 구역 접촉시작점 복귀 / 수집: 1 · A는 그대로 비활성
4보석 B와 C 수집수집: 2 / 3 → 수집: 3 / 3
5Goal 밖에서 안으로 진입LEVEL COMPLETE 한 번
6Goal에서 나왔다가 다시 진입완료 로그 추가 없음

표는 개수·복귀·완료 로그를 중심으로 읽습니다. 보석 접촉 때의 [접촉]과 [수집] 로그도 함께 출력됩니다. 이번 관찰의 핵심은 “작동했다”를 넘어 어떤 값이 바뀌었고 어떤 값이 남았는지 설명하는 것입니다.

시연 종료

Play를 종료하고 Scene을 저장합니다. Required Collectibles는 3으로 유지합니다. 조건을 바꾸는 추가 실험은 아래 선택 항목으로 남기고, 학습 정리로 넘어갑니다.

학습 정리 및 다음 실습

보석은 사라졌지만 기록은 Player에 남았습니다. 위험에 닿으면 위치가 바뀌고, 목표에 닿으면 그 기록을 읽어 결과를 정합니다. 이제 세 질문에 먼저 답한 뒤 해설을 확인하세요.

보석이 세 개인데 PlayerProgress는 왜 하나인가요?

전체 개수를 한곳에 모아야 목표가 같은 진행도를 읽을 수 있기 때문입니다. 각 보석의 collected는 그 보석을 이미 처리했는지만 기억합니다. Collectible은 PlayerProgress를 찾아 AddCollectible을 호출하고 자신을 비활성화합니다.

위험 후 돌아오는 것과 그대로 남는 것은 무엇인가요?

위치는 저장한 출발 좌표로, 속도는 복귀 순간 (0, 0)으로 바뀝니다. 수집 개수와 얻은 보석의 비활성 상태는 유지됩니다. Scene을 처음부터 다시 시작하는 규칙이 아닙니다.

목표는 무엇을 묻고, 성공을 왜 한 번만 알리나요?

HasAllCollectibles가 현재 개수 ≥ 필요 개수인지 bool로 답합니다. Goal은 그 결과로 성공을 판정하고 completed에 처리 여부를 남깁니다. 이후 재진입에서는 즉시 return하므로 완료 로그가 추가되지 않습니다.

3교시를 위한 저장 상태
항목정리할 상태
SceneWeek05_MicroLevel 저장 · Play 종료 · GuideZone 비활성화
Player기존 이동·점프 유지 · PlayerProgress 하나 · Required Collectibles 3
코드직접 작성한 네 스크립트 · Collectible 중복 class 없음 · 컴파일 오류 0
시연을 따라 연결했다면보석 3개 · 위험 1개 · 목표 1개 · PF_Collectible 저장
관찰 중심으로 참여했다면1교시 저장 장면에서 3교시 순서대로 직접 연결

3교시에서는 이미 확인한 단계는 다시 만들지 않고 연결 상태부터 검사합니다. 위험을 두 개로 늘리고 각 구역을 Prefab으로 정리한 뒤, 보석의 순서와 발판 간격을 직접 정합니다. 같은 규칙도 배치가 달라지면 다른 플레이 경험으로 이어집니다.

선택 실험 · 조건 변경

다음 실험은 50분 필수 흐름에 포함하지 않습니다. 수업 후 복습하거나 3교시의 기본 동작을 완성한 뒤 선택합니다. 한 번에 한 항목만 바꾸고 예상과 실제 결과를 비교하세요.

출발 위치를 바꾸면 어디로 돌아올까요?

Play를 종료하고 Player의 원래 위치를 기록합니다. 다른 안전한 위치로 옮긴 뒤 새 Play에서 위험에 닿습니다. 새 출발 좌표로 돌아오는지 확인합니다. 특정 좌표를 코드에 적지 않고 Start에서 읽었기 때문입니다. 실험 후 Play를 종료하고 원래 출발 위치로 복구합니다.

필요 개수를 3에서 2로 바꾸면?

Play를 종료하고 Required Collectibles만 2로 바꿉니다. 보석 세 개는 그대로 둔 채 새 Play에서 두 개를 얻고 Goal에 들어갑니다. 성공 로그와 남은 보석 한 개를 확인합니다. 설정값을 바꾸어도 collectedCount의 시작값은 0입니다. 실험 후 Play를 종료하고 필요 개수를 3으로 복구합니다.

보석 세 개에 필요 개수 4를 요구하면?

새 Play에서 세 개를 모두 얻어도 3 ≥ 4는 false이므로 성공하지 않습니다. 메서드가 실행되지 않는 오류와 현재 배치로 달성할 수 없는 규칙을 구분합니다. 실험했다면 Play를 종료하고 필요 개수를 3으로 복구합니다.

선택 심화 · Tilemap과 Layer 설정

기본 상호작용과 필수 테스트를 마친 뒤 선택합니다. 이번 필수 테스트는 기존 Ground·발판으로 통과할 수 있으며 Tilemap 여부로 판정하지 않습니다.

Tilemap · 반복되는 바닥과 발판 만들기

Tilemap은 Grid의 칸마다 Tile을 배치하는 도구입니다. Grid는 칸의 기준, Tile Asset은 한 칸의 데이터, Tile Palette는 칠하는 도구, Tilemap은 배치 기록입니다. Tilemap Renderer는 그림을 표시하고 Tilemap Collider 2D는 접촉 경계를 만듭니다.

  1. 실습 Scene 및 도구 준비

    Week05_MicroLevel을 저장한 뒤 별도 Scene에서 연습합니다. Package Manager의 Unity Registry에서 2D Tilemap Editor 설치 여부를 확인합니다. Hierarchy의 2D Object → Tilemap → Rectangular로 Grid와 Tilemap을 만듭니다.

    확인필수 실습 Scene은 그대로 남습니다.
  2. Tile Palette 구성

    4주차 Ground 이미지를 사용할 수 있습니다. Sprite Mode Single, Pixels Per Unit을 확인합니다. Window → 2D → Tile Palette에서 새 Palette를 만들고 Sprite를 드래그해 Tile Asset을 저장합니다. 한 칸보다 그림이 지나치게 크면 Sprite의 픽셀 크기와 PPU를 비교합니다.

    확인Tile Asset을 선택하고 Palette에서 붓으로 칠할 수 있습니다.
  3. Solid Tilemap 구성

    Tilemap 이름을 Solid로 바꾸고 Palette의 Active Tilemap을 Solid로 지정합니다. 작은 바닥 세 칸을 칠하고 Tilemap Collider 2D를 추가합니다. Is Trigger는 끄고 Tile Asset의 Collider Type이 None이 아닌지 확인합니다.

    확인녹색 Collider 경계가 보이고 Player가 착지합니다.
  4. 장식과 충돌 지형 분리

    두 번째 Tilemap은 Decoration으로 이름을 정하고 Collider를 추가하지 않습니다. 반복 지형의 착지를 검증한 뒤에만 기존 바닥 중 교체할 부분을 비활성화합니다. 두 바닥 Collider가 같은 자리에 겹치지 않도록 합니다.

    확인Solid는 바닥·발판·벽, Decoration은 그림만 담당합니다.

큰 맵의 접촉 경계를 합치는 Composite Collider 2D는 이번 범위에서 제외합니다. 수집·위험·목표는 별도 Prefab으로 유지합니다.

Layer · 필요한 접촉만 남기기

먼저 현재 Layer 설정으로 모든 기능이 작동하는지 확인합니다. Tags and Layers에서 Player, Environment, Interaction Layer를 만들고 Player, 모든 바닥·발판·벽, 세 종류의 상호작용에 각각 지정합니다. Layer와 Tag는 별도 항목이므로 Player Tag도 유지합니다.

Edit → Project Settings → Physics 2D의 Layer Collision Matrix에서 Player ↔ Environment와 Player ↔ Interaction을 켭니다. 실제 오브젝트가 새 Layer에 모두 배정된 뒤에만 불필요한 조합을 끕니다. Default에 남은 바닥이 있다면 그 접촉도 유지해야 합니다.

확인

수집과 착지를 다시 테스트합니다. Player와 Interaction 조합만 잠시 끄면 모든 Trigger 반응이 사라지고, 다시 켜면 복구되는지 비교합니다.

최종 코드 확인

본문의 각 작성 단계에서 추가·수정할 코드는 바로 확인하고, 적용 후 전체 코드는 필요할 때 펼쳐 볼 수 있습니다. PlayerProgress는 아래 네 버전 순서로 커지고, 최종 제출에는 마지막 목표 조건까지 포함된 코드를 사용합니다.

  1. PlayerProgress.cs · 수집 개수 저장
  2. PlayerProgress.cs · 시작 위치 저장 추가
  3. PlayerProgress.cs · 위험 복귀 추가
  4. PlayerProgress.cs · 목표 조건까지 포함한 최종 파일

상호작용 스크립트도 해당 단계의 전체 파일과 비교합니다.

Unity 공식 문서

다음 학습상호작용 레벨 구성 실습
다음 자료 →